{"contents":[{"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\nOKです!\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⇒OKです。\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⇒OKです。\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## 2段階認証無し\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\">2段階認証有り</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=\"2段階認証無し テストメール\" width=\"1079\" height=\"719\" loading=\"lazy\"></a>\n\n<br />\n\n⇒Good!  \n\n<br />\n\n<a class=\"anchor\" id=\"nidan\"></a>\n\n## 2段階認証有り\n\n<span style=\"color: red; \"><strong>2段階認証を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=\"2段階認証有り テストメール\" width=\"1079\" height=\"719\" loading=\"lazy\"></a>\n\n<br />\n\n⇒Good!  \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":"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;? から始めてもOKになります。</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":"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\nOKです。\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今回作成したのは、以下の2パターンです。\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この2パターンの環境構築について、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 の管理コンソール とは、GUIの 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 認証動作確認1\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サインイン画面1\" 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サインイン画面2\" 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サインイン画面3\" 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 認証動作確認2\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サインイン画面1\" 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サインイン画面2\" 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サインイン画面3\" 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":"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## 1\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## 2\n  \n<blockquote class=\"info\">\n<p>ここで入力する内容は、以下の関係のようです。</p>\n<p>フル ネーム : 表示名(ログイン画面の名前)</p>\n<p>ユーザー名 : システムで管理するアカウントID(chown xxx 等に使うユーザー名。)</p>\n<p>パスワード : 後述する6の画面があったとしても採用されるログインパスワード</p>\n</blockquote>\n<blockquote class=\"alert\">\n<p style=\"color: red;\">ユーザー名を「admin」とすると、後述する6の画面が表示されます。ここでは、あえて、「admin」を指定しています。</p>\n</blockquote>\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image2.png\n\"\")  \n\n<br />\n  \n## 3\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## 4\n  \n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image4.png\n\"\")  \n\n<br />\n\n  \n## 5\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## 6\n  \n<blockquote class=\"alert\">\n<p style=\"color: red;\">2でユーザー名を「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 : 2で設定したパスワードが採用され、反映されないようです。</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## 7\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>のPCの場合、なぜかここで止まりました。(2回行い、2回とも)</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# インストール完了からIPアドレス設定まで\n\n  \n## 8\n  \n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/ubuntu_install/image12.png\n\"\")  \n\n<br />\n\n2で入力した「フル ネーム」、あるいは、6で入力した「Your name」をクリックして、2で入力したパスワードでログインします。  \n<blockquote class=\"warn\">\n<p>なお、Not listed?をクリックすると、ユーザー名の入力を求められます。ここで入力するのは、2で入力した「ユーザー名」、あるいは、6で入力した「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## 9\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## 10\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## 11\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## 12\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## 13\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## 14\n  \nDHCPから取得したIPアドレスでインターネットにつながっていました。  \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## 15\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## 16\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## 17\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## 18\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## 19\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## 20\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## 21\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## 22\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## 23\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## 24\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## 25\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## 26\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## 27\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>14で「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## 28  \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 インストール 1 isoを選択して簡易インストールを使用します。 2 ここで入力する内容は、以下の関係のようです。 フル ネーム : 表示名(ログイン画面の名前) ユーザー名 : システムで管理するアカウントID(chown xxx 等に使うユーザー名。) パスワード : 後述する6の画面があったとしても採用されるログインパスワード ユーザー名を「admin」とすると、後述する6の画面が表示されます。ここでは、あえて、「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":"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]_[文字]というファイルを1個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をファイル名に使用する版(1個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":"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| ID | ユーザー名 | 役割 |\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":"dikzpvtybk","createdAt":"2021-06-19T10:13:40.875Z","updatedAt":"2021-07-16T10:19:45.857Z","publishedAt":"2021-06-19T10:13:40.875Z","revisedAt":"2021-07-16T10:19:45.857Z","title":"GitLab,Wekan→Rocket.Chat→SlackのWebHook","category":{"id":"2ijwygxyjj","createdAt":"2021-06-19T10:12:08.920Z","updatedAt":"2021-08-31T12:03:42.373Z","publishedAt":"2021-06-19T10:12:08.920Z","revisedAt":"2021-08-31T12:03:42.373Z","topics":"WebHook","logo":"/logos/WebHook.png","needs_title":false},"topics":[{"id":"2ijwygxyjj","createdAt":"2021-06-19T10:12:08.920Z","updatedAt":"2021-08-31T12:03:42.373Z","publishedAt":"2021-06-19T10:12:08.920Z","revisedAt":"2021-08-31T12:03:42.373Z","topics":"WebHook","logo":"/logos/WebHook.png","needs_title":false},{"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":"gg2a3kv3ofiu","createdAt":"2021-05-09T08:35:47.263Z","updatedAt":"2021-05-09T08:35:47.263Z","publishedAt":"2021-05-09T08:35:47.263Z","revisedAt":"2021-05-09T08:35:47.263Z","topics":"Wekan","logo":"/logos/Wekan.png","needs_title":false},{"id":"phvhl1peuv","createdAt":"2021-05-09T08:35:20.401Z","updatedAt":"2021-08-31T12:05:36.709Z","publishedAt":"2021-05-09T08:35:20.401Z","revisedAt":"2021-08-31T12:05:36.709Z","topics":"RocketChat","logo":"/logos/RocketChat.png","needs_title":false}],"content":"# はじめに\n先日  \n・GitLab  \n・Wekan  \n・Rocket.Chat  \nをローカルの環境にインストールしました。  \n<br />\n※インストールについては、下記別記事を参照してください。  \n<a href=\"https://itc-engineering-blog.netlify.app/blogs/fgm-dkbu10\" target=\"_blank\">「Ubuntu 20.04.2.0にGitLabをインストール」</a>  \n<a href=\"https://itc-engineering-blog.netlify.app/blogs/1ff2yl5tu\" target=\"_blank\">「CentOS8にwekanをインストール(ソースからビルド編)」</a>  \n<a href=\"https://itc-engineering-blog.netlify.app/blogs/b16k2cqlw\" target=\"_blank\">「CentOS8にRocket.Chatをインストール」</a>  \n<br />\nこの環境は自宅や外出時のスマホなどのインターネットから直接見られません。  \nそこで、インターネットからチャットの内容、GitLab,Wekanのアクティビティについて確認できるようWebHookを利用して、以下の環境を構築しました。  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/webhook.svg)\n\n<br />\n\nSlackはインターネットのビジネスチャットサービスで、10000通まで無料です。(2021/06現在)  \nSlackについては、WebHookの手順以外、今回解説しません。\n\n<blockquote class=\"info\">\n<p><strong>【 WebHook 】</strong></p>\n<p>「WebHook」とは、データ更新などのイベントが発生したら、外部のアプリやサービスに通知を送り、連携する仕組みのことです。</p>\n</blockquote>\n\n<br />\n\n・[SlackにIncoming WebHookを設定](#anchor1)  \n・[Rocket.ChatにOutgoing WebHookを設定](#anchor2)  \n・[Rocket.ChatにIncoming WebHook(GitLab用)を設定](#anchor3)  \n・[GitLabにOutgoing WebHookを設定](#anchor4)  \n・[Rocket.ChatにIncoming WebHook(Wekan用)を設定](#anchor5)  \n・[WekanにOutgoing WebHookを設定](#anchor6)  \nまで全手順をご紹介していきます。\n\n<br />\n\n<a class=\"anchor\" id=\"anchor1\"></a>\n\n# SlackにIncoming WebHookを設定\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/webhook-step1.svg)\n\n<br />\n\nSlackにログインして、Slackのアプリケーション追加ページを開きます。  \n<a href=\"https://slack.com/apps\" target=\"_blank\">https://slack.com/apps</a>  \n\n「Webhook」と入力して、「Incoming WebHook」を選択します。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image1.png\" alt=\"\" width=\"600px\">\n<br />\n\n「Slackに追加」をクリックします。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image2.png\" alt=\"\" width=\"500px\">\n<br />\n\nチャンネルを追加します。  \n→ここで選択したチャンネルがRocket.Chatから受信するチャンネルになります。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image3.png\" alt=\"\" width=\"400px\">\n<br />\n\n「Incoming WebHook インテグレーションの追加」をクリックします。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image4.png\" alt=\"\" width=\"400px\">\n<br />\n\nここで、「Webhook URL」をコピーしておきます。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image5.png\" alt=\"\" width=\"500px\">\n<br />\n\n「設定を保存する」をクリックします。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image6.png\" alt=\"\" width=\"500px\">\n<br />\n\n以上で、Slackの設定は完了です。  \n\n<br />\n\n<a class=\"anchor\" id=\"anchor2\"></a>\n\n# Rocket.ChatにOutgoing WebHookを設定\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/webhook-step2.svg)\n\n<br />\n\nRocket.Chatに管理者でログイン後、ユーザーのアイコンをクリックして、「管理」をクリックします。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image7.png\" alt=\"\" width=\"600px\">\n<br />\n\n「サービス連携」をクリックします。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image8.png\" alt=\"\" width=\"300px\">\n<br />\n\n「+New」をクリックし、「Outgoing」を選択します。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image9-plus.png\" alt=\"\" width=\"600px\">\n<br />\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image9.png\" alt=\"\" width=\"600px\">\n<br />\n\n「イベントトリガー」は、\"メッセージが送信されました\"とし、Rocket.Chatでメッセージが書き込まれるたびにWebHookが作動するようにします。  \n「名前」は、\"Slack\"とします。  \nRocket.Chatの#generalチャンネルからSlackの #rocketchat チャンネルへ伝播したいので、チャンネルは、 #general に設定します。  \n「URLs」のところは、先ほど、Slackの設定でコピーしたWebhook URLを貼り付けます。  \n「投稿ユーザー」は、Rocket.Chatで登録されているユーザー名を入力します。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image10.png\" alt=\"\" width=\"600px\">\n<br />\n\n「トークン」は、今回特に意味は無いため、入力されたままにします。  \nScriptのところに、以下の内容のスクリプトを貼り付けます。  \n\n<br />\n\n・`Script`  \n<details><summary><strong>クリックしてソースを表示</strong></summary>\n\n```javascript\nclass Script {\n  /**\n   * @params {object} request\n   */\n  prepare_outgoing_request({ request }) {\n    // request.params            {object}\n    // request.method            {string}\n    // request.url               {string}\n    // request.auth              {string}\n    // request.headers           {object}\n    // request.data.token        {string}\n    // request.data.channel_id   {string}\n    // request.data.channel_name {string}\n    // request.data.timestamp    {date}\n    // request.data.user_id      {string}\n    // request.data.user_name    {string}\n    // request.data.text         {string}\n    // request.data.trigger_word {string}\n    return {\n      url: request.url,\n      headers: request.headers,\n      method: 'POST',\n      data: {\n        channel: '#rocketchat',\n        username: request.data.alias\n          ? request.data.alias\n          : request.data.user_name,\n        text: request.data.text,\n        icon_emoji: ':ghost:',\n      },\n    };\n  }\n\n  /**\n   * @params {object} request, response\n   */\n  process_outgoing_response({ request, response }) {\n    // request              {object} - the object returned by prepare_outgoing_request\n\n    // response.error       {object}\n    // response.status_code {integer}\n    // response.content     {object}\n    // response.content_raw {string/object}\n    // response.headers     {object}\n\n    // Return false will abort the response\n    return false;\n  }\n}\n```\n\n</details>\n\n<blockquote class=\"warn\">\n<p><code>prepare_outgoing_request</code>の<code>return</code>のところの<code>text:</code>を<code>message:</code>とすると、Rocket.Chatに投稿されて無限ループしました。Rocket.Chatに投稿→WebHook発動→WebHookからRocket.Chatに投稿→Rocket.Chatに投稿...となってしまいました。</p>\n</blockquote>\n\n<br />\n\nScriptの  \n`channel`は、伝播させたいSlackのチャンネル名  \n`username`は、Slackに投稿するユーザー名  \n`text`は、Slackに投稿する内容  \n`icon_emoji`は、Slackに表示されるアイコンを絵文字コードで指定  \nの意味になります。\n\n```javascript\n      data: {\n        channel: '#rocketchat',\n        username: request.data.alias\n          ? request.data.alias\n          : request.data.user_name,\n        text: request.data.text,\n        icon_emoji: ':ghost:',\n      },\n```\n\n<br />\n\n`icon_emoji:` の絵文字コードですが、<a href=\"https://www.webfx.com/tools/emoji-cheat-sheet\" target=\"_blank\">https://www.webfx.com/tools/emoji-cheat-sheet/</a> 等で分かりますので、`:ghost:`に限らず、`:smile:`などに変更もできます。\n\n<br />\n\n**「有効」、「スクリプトを有効にする」スイッチをオン**にして、保存ボタンをクリックします。  \n\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image11.png\" alt=\"\" width=\"600px\">\n<br />\n\n以上で、Rocket.Chat→Slackの設定は完了です。\n\n<br />\n\n<a class=\"anchor\" id=\"anchor3\"></a>\n\n# Rocket.ChatにIncoming WebHook(GitLab用)を設定\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/webhook-step3.svg)\n\n<br />\n\nRocket.Chatに管理者でログイン後、ユーザーのアイコンをクリックして、「管理」をクリックします。  \n「サービス連携」をクリックします。  \n「Incoming」を選択して、「+New」をクリックします。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image12.png\" alt=\"\" width=\"600px\">\n<br />\n\n「名前」は、「GitLab」とし、投稿先チャンネルは、 #general とします。  \n「投稿ユーザー」は、Rocket.Chatで登録されているユーザー名を入力します。  \n「エイリアス」は、ユーザー名の前に表示されるのですが、今回スクリプトを登録するため、反映されません。\n「絵文字」は、アバターの画像になります。<a href=\"https://www.webfx.com/tools/emoji-cheat-sheet\" target=\"_blank\">https://www.webfx.com/tools/emoji-cheat-sheet/</a> 等で調べて、絵文字コードを入力します。今回スクリプトを登録するため、通常、GitLabのユーザーアイコンが表示されます。\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image13.png\" alt=\"\" width=\"600px\">\n<br />\n\nScriptのところに、以下の内容のスクリプトを貼り付けます。  \n\n<br />\n\n・`Script`  \n<details><summary><strong>クリックしてソースを表示</strong></summary>\n\n```javascript\n/* eslint no-console:0, max-len:0, complexity:0 */\n// see https://gitlab.com/help/web_hooks/web_hooks for full json posted by GitLab\nconst MENTION_ALL_ALLOWED = false; // <- check that bot permission 'mention-all' are activated in rocketchat before passing this to true.\nconst NOTIF_COLOR = '#6498CC';\nconst IGNORE_CONFIDENTIAL = true;\nconst IGNORE_UNKNOWN_EVENTS = false;\nconst IGNORE_ERROR_MESSAGES = false;\nconst USE_ROCKETCHAT_AVATAR = false;\nconst DEFAULT_AVATAR = null; // <- null means use the avatar from settings if no other is available\nconst STATUSES_COLORS = {\n\tsuccess: '#2faa60',\n\tpending: '#e75e40',\n\tfailed: '#d22852',\n\tcanceled: '#5c5c5c',\n\tcreated: '#ffc107',\n\trunning: '#607d8b',\n};\nconst ACTION_VERBS = {\n\tcreate: 'created',\n\tdestroy: 'removed',\n\tupdate: 'updated',\n\trename: 'renamed',\n\ttransfer: 'transferred',\n\tadd: 'added',\n\tremove: 'removed',\n};\nconst ATTACHMENT_TITLE_SIZE = 10; // Put 0 here to have not title as in previous versions\nconst refParser = (ref) => ref.replace(/^refs\\/(?:tags|heads)\\/(.+)$/, '$1');\nconst displayName = (name) => (name && name.toLowerCase().replace(/\\s+/g, '.').normalize('NFD').replace(/[\\u0300-\\u036f]/g, ''));\nconst atName = (user) => (user && user.name ? '@' + displayName(user.name) : '');\nconst makeAttachment = (author, text, timestamp, color) => {\n\tconst currentTime = (new Date()).toISOString();\n\tconst attachment = {\n\t\tauthor_name: author ? displayName(author.name) : '',\n\t\tauthor_icon: author ? author.avatar_url : '',\n\t\tts: timestamp || currentTime,\n\t\ttext,\n\t\tcolor: color || NOTIF_COLOR\n\t};\n\tif (ATTACHMENT_TITLE_SIZE > 0) {\n\t\tattachment.title = text.substring(0, ATTACHMENT_TITLE_SIZE) + '...';\n\t}\n\n\treturn attachment;\n};\nconst pushUniq = (array, val) => ~array.indexOf(val) || array.push(val); // eslint-disable-line\n\nclass Script { // eslint-disable-line\n\tprocess_incoming_request({ request }) {\n\t\ttry {\n\t\t\tlet result = null;\n\t\t\tconst channel = request.url.query.channel;\n\t\t\tconst event = request.headers['x-gitlab-event'];\n\t\t\tswitch (event) {\n\t\t\t\tcase 'Push Hook':\n\t\t\t\t\tresult = this.pushEvent(request.content);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'Merge Request Hook':\n\t\t\t\t\tresult = this.mergeRequestEvent(request.content);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'Note Hook':\n\t\t\t\t\tresult = this.commentEvent(request.content);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'Confidential Issue Hook':\n\t\t\t\tcase 'Issue Hook':\n\t\t\t\t\tresult = this.issueEvent(request.content, event);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'Tag Push Hook':\n\t\t\t\t\tresult = this.tagEvent(request.content);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'Pipeline Hook':\n\t\t\t\tcase 'Pipeline Event':\n\t\t\t\t\tresult = this.pipelineEvent(request.content);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'Build Hook': // GitLab < 9.3.0\n\t\t\t\tcase 'Job Hook': // GitLab >= 9.3.0\n\t\t\t\t\tresult = this.buildEvent(request.content);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'Wiki Page Hook':\n\t\t\t\t\tresult = this.wikiEvent(request.content);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'System Hook':\n\t\t\t\t\tresult = this.systemEvent(request.content);\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tif (IGNORE_UNKNOWN_EVENTS) {\n\t\t\t\t\t\tconsole.log('gitlabevent unknown', event);\n\t\t\t\t\t\treturn { error: { success: false, message: `unknonwn event ${event}` } };\n\t\t\t\t\t}\n\t\t\t\t\tresult = IGNORE_UNKNOWN_EVENTS ? null : this.unknownEvent(request, event);\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (result && result.content && channel) {\n\t\t\t\tresult.content.channel = '#' + channel;\n\t\t\t}\n\t\t\treturn result;\n\t\t} catch (e) {\n\t\t\tconsole.log('gitlabevent error', e);\n\t\t\treturn this.createErrorChatMessage(e);\n\t\t}\n\t}\n\n\tcreateErrorChatMessage(error) {\n\t\tif (IGNORE_ERROR_MESSAGES) {\n\t\t\treturn { error: { success: false, message: `gitlabevent error: ${error.message}` } };\n\t\t}\n\t\treturn {\n\t\t\tcontent: {\n\t\t\t\tusername: 'Rocket.Cat ErrorHandler',\n\t\t\t\t//text: 'Error occured while parsing an incoming webhook request. Details attached.',\n\t\t\t\ttext:\n\t\t\t\t\t'Error occured while parsing an incoming webhook request. Details attached.\\n' +\n\t\t\t\t\t`Error: '${error}', \\n Message: '${error.message}', \\n Stack: '${error.stack}'`,\n\t\t\t\ticon_url: USE_ROCKETCHAT_AVATAR ? null : DEFAULT_AVATAR,\n\t\t\t\t// attachments: [\n\t\t\t\t// \t{\n\t\t\t\t// \t\ttext: `Error: '${error}', \\n Message: '${error.message}', \\n Stack: '${error.stack}'`,\n\t\t\t\t// \t\tcolor: NOTIF_COLOR\n\t\t\t\t// \t}\n\t\t\t\t// ]\n\t\t\t}\n\t\t};\n\t}\n\n\tunknownEvent(data, event) {\n\t\tconst user_avatar = data.user ? data.user.avatar_url : (data.user_avatar || DEFAULT_AVATAR);\n\t\treturn {\n\t\t\tcontent: {\n\t\t\t\tusername: data.user ? data.user.name : (data.user_name || 'Unknown user'),\n\t\t\t\t//text: `Unknown event '${event}' occured. Data attached.`,\n\t\t\t\ttext: `Unknown event '${event}' occured. Data attached.\\n${JSON.stringify(\n\t\t\t\t\tdata,\n\t\t\t\t\tnull,\n\t\t\t\t\t4\n\t\t\t\t)}`,\n\t\t\t\ticon_url: USE_ROCKETCHAT_AVATAR ? null : user_avatar,\n\t\t\t\t// attachments: [\n\t\t\t\t// \t{\n\t\t\t\t// \t\ttext: `${JSON.stringify(data, null, 4)}`,\n\t\t\t\t// \t\tcolor: NOTIF_COLOR\n\t\t\t\t// \t}\n\t\t\t\t// ]\n\t\t\t}\n\t\t};\n\t}\n\tissueEvent(data, event) {\n\t\tif (event === 'Confidential Issue Hook' && IGNORE_CONFIDENTIAL) {\n\t\t\treturn false;\n\t\t}\n\t\tconst project = data.project || data.repository;\n\t\tconst state = data.object_attributes.state;\n\t\tconst action = data.object_attributes.action;\n\t\tconst time = data.object_attributes.updated_at;\n\t\tconst project_avatar = project.avatar_url || data.user.avatar_url || DEFAULT_AVATAR;\n\t\tlet user_action = state;\n\t\tlet assigned = '';\n\n\t\tif (action === 'update') {\n\t\t\tuser_action = 'updated';\n\t\t}\n\n\t\tif (data.assignee) {\n\t\t\tassigned = `*Assigned to*: @${data.assignee.username}\\n`;\n\t\t}\n\n\t\treturn {\n\t\t\tcontent: {\n\t\t\t\tusername: 'gitlab/' + project.name,\n\t\t\t\ticon_url: USE_ROCKETCHAT_AVATAR ? null : project_avatar,\n\t\t\t\t// text: (data.assignee && data.assignee.name !== data.user.name) ? atName(data.assignee) : '',\n\t\t\t\ttext:\n\t\t\t\t\t`*${data.user ? displayName(data.user.name) : ''}*\\n` +\n\t\t\t\t\t'担当者:' +\n\t\t\t\t\t(data.assignee && data.assignee.name !== data.user.name\n\t\t\t\t\t\t? atName(data.assignee)\n\t\t\t\t\t\t: '無し') +\n\t\t\t\t\t`\\n${user_action} an issue _${data.object_attributes.title}_ on ${project.name}.\n\t\t\t\t\t*Description:* ${data.object_attributes.description}.\n\t\t\t\t\t${assigned}\n\t\t\t\t\tSee: ${data.object_attributes.url}`,\n\t\t\t\t// \t\t\t\tattachments: [\n\t\t\t\t// \t\t\t\t\tmakeAttachment(\n\t\t\t\t// \t\t\t\t\t\tdata.user,\n\t\t\t\t// \t\t\t\t\t\t`${user_action} an issue _${data.object_attributes.title}_ on ${project.name}.\n\t\t\t\t// *Description:* ${data.object_attributes.description}.\n\t\t\t\t// ${assigned}\n\t\t\t\t// See: ${data.object_attributes.url}`,\n\t\t\t\t// \t\t\t\t\t\ttime\n\t\t\t\t// \t\t\t\t\t)\n\t\t\t\t// \t\t\t\t]\n\t\t\t}\n\t\t};\n\t}\n\n\tcommentEvent(data) {\n\t\tconst project = data.project || data.repository;\n\t\tconst comment = data.object_attributes;\n\t\tconst user = data.user;\n\t\tconst avatar = project.avatar_url || user.avatar_url || DEFAULT_AVATAR;\n\t\tconst at = [];\n\t\tlet text;\n\t\tif (data.merge_request) {\n\t\t\tconst mr = data.merge_request;\n\t\t\tconst lastCommitAuthor = mr.last_commit && mr.last_commit.author;\n\t\t\tif (mr.assignee && mr.assignee.name !== user.name) {\n\t\t\t\tat.push(atName(mr.assignee));\n\t\t\t}\n\t\t\tif (lastCommitAuthor && lastCommitAuthor.name !== user.name) {\n\t\t\t\tpushUniq(at, atName(lastCommitAuthor));\n\t\t\t}\n\t\t\ttext = `commented on MR [#${mr.id} ${mr.title}](${comment.url})`;\n\t\t} else if (data.commit) {\n\t\t\tconst commit = data.commit;\n\t\t\tconst message = commit.message.replace(/\\n[^\\s\\S]+/, '...').replace(/\\n$/, '');\n\t\t\tif (commit.author && commit.author.name !== user.name) {\n\t\t\t\tat.push(atName(commit.author));\n\t\t\t}\n\t\t\ttext = `commented on commit [${commit.id.slice(0, 8)} ${message}](${comment.url})`;\n\t\t} else if (data.issue) {\n\t\t\tconst issue = data.issue;\n\t\t\ttext = `commented on issue [#${issue.id} ${issue.title}](${comment.url})`;\n\t\t} else if (data.snippet) {\n\t\t\tconst snippet = data.snippet;\n\t\t\ttext = `commented on code snippet [#${snippet.id} ${snippet.title}](${comment.url})`;\n\t\t}\n\t\treturn {\n\t\t\tcontent: {\n\t\t\t\tusername: 'gitlab/' + project.name,\n\t\t\t\ticon_url: USE_ROCKETCHAT_AVATAR ? null : avatar,\n\t\t\t\t//text: at.join(' '),\n\t\t\t\ttext:\n\t\t\t\t\t`*${user ? displayName(user.name) : ''}*\\n` +\n\t\t\t\t\t'担当者:' +\n\t\t\t\t\tat.join(' ') +\n\t\t\t\t\t`\\n${text}\\n${comment.note}`,\n\t\t\t\t// attachments: [\n\t\t\t\t// \tmakeAttachment(user, `${text}\\n${comment.note}`, comment.updated_at)\n\t\t\t\t// ]\n\t\t\t}\n\t\t};\n\t}\n\n\tmergeRequestEvent(data) {\n\t\tconst user = data.user;\n\t\tconst mr = data.object_attributes;\n\t\tconst assignee = data.assignee;\n\t\tconst avatar = mr.target.avatar_url || mr.source.avatar_url || user.avatar_url || DEFAULT_AVATAR;\n\t\tlet at = [];\n\n\t\tif (mr.action === 'open' && assignee) {\n\t\t\tat.push(atName(assignee));\n\t\t} else if (mr.action === 'merge') {\n\t\t\tconst lastCommitAuthor = mr.last_commit && mr.last_commit.author;\n\t\t\tif (assignee && assignee.username !== user.username) {\n\t\t\t\tat.push(atName(assignee));\n\t\t\t}\n\t\t\tif (lastCommitAuthor && lastCommitAuthor.username !== user.username) {\n\t\t\t\tpushUniq(at, atName(lastCommitAuthor));\n\t\t\t}\n\t\t} else if (mr.action === 'update' && assignee) {\n\t\t\tat.push(atName(assignee));\n\t\t}\n\t\treturn {\n\t\t\tcontent: {\n\t\t\t\tusername: `gitlab/${mr.target.name}`,\n\t\t\t\ticon_url: USE_ROCKETCHAT_AVATAR ? null : avatar,\n\t\t\t\t//text: at.join(' '),\n\t\t\t\ttext:\n\t\t\t\t\t`*${user ? displayName(user.name) : ''}*\\n` +\n\t\t\t\t\t'担当者:' +\n\t\t\t\t\tat.join(' ') +\n\t\t\t\t\t`\\n${mr.action} MR [#${mr.iid} ${mr.title}](${mr.url})\\n${mr.source_branch} into ${mr.target_branch}`,\n\t\t\t\t// attachments: [\n\t\t\t\t// \tmakeAttachment(user, `${mr.action} MR [#${mr.iid} ${mr.title}](${mr.url})\\n${mr.source_branch} into ${mr.target_branch}`, mr.updated_at)\n\t\t\t\t// ]\n\t\t\t}\n\t\t};\n\t}\n\n\tpushEvent(data) {\n\t\tconst project = data.project || data.repository;\n\t\tconst web_url = project.web_url || project.homepage;\n\t\tconst user = {\n\t\t\tname: data.user_name,\n\t\t\tavatar_url: data.user_avatar\n\t\t};\n\t\tconst avatar = project.avatar_url || data.user_avatar || DEFAULT_AVATAR;\n\t\t// branch removal\n\t\tif (data.checkout_sha === null && !data.commits.length) {\n\t\t\treturn {\n\t\t\t\tcontent: {\n\t\t\t\t\ttext: `*${user ? displayName(user.name) : ''\n\t\t\t\t\t\t}*\\n\\`removed branch ${refParser(data.ref)} from [${project.name\n\t\t\t\t\t\t}](${web_url})\\``,\n\t\t\t\t\tusername: `gitlab/${project.name}`,\n\t\t\t\t\ticon_url: USE_ROCKETCHAT_AVATAR ? null : avatar,\n\t\t\t\t\t// attachments: [\n\t\t\t\t\t// \tmakeAttachment(user, `removed branch ${refParser(data.ref)} from [${project.name}](${web_url})`)\n\t\t\t\t\t// ]\n\t\t\t\t}\n\t\t\t};\n\t\t}\n\t\t// new branch\n\t\tif (data.before == 0) { // eslint-disable-line\n\t\t\treturn {\n\t\t\t\tcontent: {\n\t\t\t\t\ttext: `*${user ? displayName(user.name) : ''\n\t\t\t\t\t\t}*\\n\\`pushed new branch [${refParser(\n\t\t\t\t\t\t\tdata.ref\n\t\t\t\t\t\t)}](${web_url}/commits/${refParser(data.ref)}) to [${project.name\n\t\t\t\t\t\t}](${web_url}), which is ${data.total_commits_count\n\t\t\t\t\t\t} commits ahead of master\\``,\n\t\t\t\t\tusername: `gitlab/${project.name}`,\n\t\t\t\t\ticon_url: USE_ROCKETCHAT_AVATAR ? null : avatar,\n\t\t\t\t\t// attachments: [\n\t\t\t\t\t// \tmakeAttachment(user, `pushed new branch [${refParser(data.ref)}](${web_url}/commits/${refParser(data.ref)}) to [${project.name}](${web_url}), which is ${data.total_commits_count} commits ahead of master`)\n\t\t\t\t\t// ]\n\t\t\t\t}\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tcontent: {\n\t\t\t\ttext:\n\t\t\t\t\t`*${user ? displayName(user.name) : ''}*\\n\\`pushed ${data.total_commits_count\n\t\t\t\t\t} commits to branch [${refParser(\n\t\t\t\t\t\tdata.ref\n\t\t\t\t\t)}](${web_url}/commits/${refParser(data.ref)}) in [${project.name\n\t\t\t\t\t}](${web_url})\\`\\n\\n` +\n\t\t\t\t\tdata.commits\n\t\t\t\t\t\t.map(\n\t\t\t\t\t\t\t(commit) =>\n\t\t\t\t\t\t\t\t`  - ${new Date(commit.timestamp).toUTCString()} [${commit.id.slice(\n\t\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\t\t8\n\t\t\t\t\t\t\t\t)}](${commit.url}) by ${commit.author.name\n\t\t\t\t\t\t\t\t}: ${commit.message.replace(/\\s*$/, '')}`\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.join('\\n'),\n\t\t\t\tusername: `gitlab/${project.name}`,\n\t\t\t\ticon_url: USE_ROCKETCHAT_AVATAR ? null : avatar,\n\t\t\t\t// attachments: [\n\t\t\t\t// \tmakeAttachment(user, `pushed ${data.total_commits_count} commits to branch [${refParser(data.ref)}](${web_url}/commits/${refParser(data.ref)}) in [${project.name}](${web_url})`),\n\t\t\t\t// \t{\n\t\t\t\t// \t\ttext: data.commits.map((commit) => `  - ${new Date(commit.timestamp).toUTCString()} [${commit.id.slice(0, 8)}](${commit.url}) by ${commit.author.name}: ${commit.message.replace(/\\s*$/, '')}`).join('\\n'),\n\t\t\t\t// \t\tcolor: NOTIF_COLOR\n\t\t\t\t// \t}\n\t\t\t\t// ]\n\t\t\t}\n\t\t};\n\t}\n\n\ttagEvent(data) {\n\t\tconst project = data.project || data.repository;\n\t\tconst web_url = project.web_url || project.homepage;\n\t\tconst tag = refParser(data.ref);\n\t\tconst user = {\n\t\t\tname: data.user_name,\n\t\t\tavatar_url: data.user_avatar\n\t\t};\n\t\tconst avatar = project.avatar_url || data.user_avatar || DEFAULT_AVATAR;\n\t\tlet message;\n\t\tif (data.checkout_sha === null) {\n\t\t\tmessage = `deleted tag [${tag}](${web_url}/tags/)`;\n\t\t} else {\n\t\t\tmessage = `pushed tag [${tag} ${data.checkout_sha.slice(0, 8)}](${web_url}/tags/${tag})`;\n\t\t}\n\t\treturn {\n\t\t\tcontent: {\n\t\t\t\tusername: `gitlab/${project.name}`,\n\t\t\t\ticon_url: USE_ROCKETCHAT_AVATAR ? null : avatar,\n\t\t\t\t//text: MENTION_ALL_ALLOWED ? '@all' : '',\n\t\t\t\ttext: `*${user ? displayName(user.name) : ''}*\\n` + message,\n\t\t\t\t// text: MENTION_ALL_ALLOWED ? '@all' : '',\n\t\t\t\t// attachments: [\n\t\t\t\t// \tmakeAttachment(user, message)\n\t\t\t\t// ]\n\t\t\t}\n\t\t};\n\t}\n\n\tpipelineEvent(data) {\n\t\tconst project = data.project || data.repository;\n\t\tconst commit = data.commit;\n\t\tconst user = {\n\t\t\tname: data.user_name,\n\t\t\tavatar_url: data.user_avatar\n\t\t};\n\t\tconst pipeline = data.object_attributes;\n\t\tconst pipeline_time = pipeline.finished_at || pipeline.created_at;\n\t\tconst avatar = project.avatar_url || data.user_avatar || DEFAULT_AVATAR;\n\n\t\treturn {\n\t\t\tcontent: {\n\t\t\t\tusername: `gitlab/${project.name}`,\n\t\t\t\ticon_url: USE_ROCKETCHAT_AVATAR ? null : avatar,\n\t\t\t\ttext: `*${user ? displayName(user.name) : ''}*\\npipeline returned *${pipeline.status\n\t\t\t\t\t}* for commit [${commit.id.slice(0, 8)}](${commit.url}) made by *${commit.author.name\n\t\t\t\t\t}*`,\n\t\t\t\t// attachments: [\n\t\t\t\t// \tmakeAttachment(user, `pipeline returned *${pipeline.status}* for commit [${commit.id.slice(0, 8)}](${commit.url}) made by *${commit.author.name}*`, pipeline_time, STATUSES_COLORS[pipeline.status])\n\t\t\t\t// ]\n\t\t\t}\n\t\t};\n\t}\n\n\tbuildEvent(data) {\n\t\tconst user = {\n\t\t\tname: data.user_name,\n\t\t\tavatar_url: data.user_avatar\n\t\t};\n\n\t\treturn {\n\t\t\tcontent: {\n\t\t\t\tusername: `gitlab/${data.repository.name}`,\n\t\t\t\ticon_url: USE_ROCKETCHAT_AVATAR ? null : DEFAULT_AVATAR,\n\t\t\t\ttext: `*${user ? displayName(user.name) : ''}*\\nbuild named *${data.build_name\n\t\t\t\t\t}* returned *${data.build_status}* for [${data.project_name}](${data.repository.homepage\n\t\t\t\t\t})`,\n\t\t\t\t// attachments: [\n\t\t\t\t// \tmakeAttachment(user, `build named *${data.build_name}* returned *${data.build_status}* for [${data.project_name}](${data.repository.homepage})`, null, STATUSES_COLORS[data.build_status])\n\t\t\t\t// ]\n\t\t\t}\n\t\t};\n\t}\n\n\twikiPageTitle(wiki_page) {\n\t\tif (wiki_page.action === 'delete') {\n\t\t\treturn wiki_page.title;\n\t\t}\n\n\t\treturn `[${wiki_page.title}](${wiki_page.url})`;\n\t}\n\n\twikiEvent(data) {\n\t\tconst user_name = data.user.name;\n\t\tconst project = data.project;\n\t\tconst project_path = project.path_with_namespace;\n\t\tconst wiki_page = data.object_attributes;\n\t\tconst wiki_page_title = this.wikiPageTitle(wiki_page);\n\t\tconst user_action = ACTION_VERBS[wiki_page.action] || 'modified';\n\t\tconst avatar = project.avatar_url || data.user.avatar_url || DEFAULT_AVATAR;\n\n\t\treturn {\n\t\t\tcontent: {\n\t\t\t\tusername: project_path,\n\t\t\t\ticon_url: USE_ROCKETCHAT_AVATAR ? null : avatar,\n\t\t\t\ttext: `The wiki page ${wiki_page_title} was ${user_action} by ${user_name}`\n\t\t\t}\n\t\t};\n\t}\n\n\tsystemEvent(data) {\n\t\tconst event_name = data.event_name;\n\t\tconst [, eventType] = data.event_name.split('_');\n\t\tconst action = eventType in ACTION_VERBS ? ACTION_VERBS[eventType] : '';\n\t\tlet text = '';\n\t\tswitch (event_name) {\n\t\t\tcase 'project_create':\n\t\t\tcase 'project_destroy':\n\t\t\tcase 'project_update':\n\t\t\t\ttext = `Project \\`${data.path_with_namespace}\\` ${action}.`;\n\t\t\t\tbreak;\n\t\t\tcase 'project_rename':\n\t\t\tcase 'project_transfer':\n\t\t\t\ttext = `Project \\`${data.old_path_with_namespace}\\` ${action} to \\`${data.path_with_namespace}\\`.`;\n\t\t\t\tbreak;\n\t\t\tcase 'user_add_to_team':\n\t\t\tcase 'user_remove_from_team':\n\t\t\t\ttext = `User \\`${data.user_username}\\` was ${action} to project \\`${data.project_path_with_namespace}\\` with \\`${data.project_access}\\` access.`;\n\t\t\t\tbreak;\n\t\t\tcase 'user_add_to_group':\n\t\t\tcase 'user_remove_from_group':\n\t\t\t\ttext = `User \\`${data.user_username}\\` was ${action} to group \\`${data.group_path}\\` with \\`${data.group_access}\\` access.`;\n\t\t\t\tbreak;\n\t\t\tcase 'user_create':\n\t\t\tcase 'user_destroy':\n\t\t\t\ttext = `User \\`${data.username}\\` was ${action}.`;\n\t\t\t\tbreak;\n\t\t\tcase 'user_rename':\n\t\t\t\ttext = `User \\`${data.old_username}\\` was ${action} to \\`${data.username}\\`.`;\n\t\t\t\tbreak;\n\t\t\tcase 'key_create':\n\t\t\tcase 'key_destroy':\n\t\t\t\ttext = `Key \\`${data.username}\\` was ${action}.`;\n\t\t\t\tbreak;\n\t\t\tcase 'group_create':\n\t\t\tcase 'group_destroy':\n\t\t\t\ttext = `Group \\`${data.path}\\` was ${action}.`;\n\t\t\t\tbreak;\n\t\t\tcase 'group_rename':\n\t\t\t\ttext = `Group \\`${data.old_full_path}\\` was ${action} to \\`${data.full_path}\\`.`;\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\ttext = 'Unknown system event';\n\t\t\t\tbreak;\n\t\t}\n\n\t\treturn {\n\t\t\tcontent: {\n\t\t\t\t//text: `${text}`,\n\t\t\t\ttext: `${text}\\n${JSON.stringify(data, null, 4)}`,\n\t\t\t\t// attachments: [\n\t\t\t\t// \t{\n\t\t\t\t// \t\ttext: `${JSON.stringify(data, null, 4)}`,\n\t\t\t\t// \t\tcolor: NOTIF_COLOR\n\t\t\t\t// \t}\n\t\t\t\t// ]\n\t\t\t}\n\t\t};\n\t}\n}\n```\n\n</details>\n\n<blockquote class=\"warn\">\n<p>スクリプトは、<a href=\"https://github.com/malko/rocketchat-gitlab-hook\">githubのmalko/rocketchat-gitlab-hook</a>から拝借してそのまま使おうと思ったのですが、<code>attachments</code>だけ<code>return</code>すると、なぜかOutgoing WebHookが反応しなかったため、<code>attachments</code>の内容が<code>text</code>に載るようにして、反応させました。</p>\n<p>ソースコードは、githubにも置きました。→リンク:<a href=\"https://github.com/itc-lab/rocketchat-gitlab-hook-kai\">itc-lab/rocketchat-gitlab-hook-kai<a></p>\n<p>なお、スクリプトは、JavaScriptエラーの場合、無反応になるようですが、単純に <code>console.log</code> を出力させるだけにしても動きませんでした。</p>\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/webhook-notice.svg\" alt=\"\" width=\"600px\">\n</blockquote>\n\n<br />\n\n**「有効」、「スクリプトを有効にする」スイッチをオン**にして、保存ボタンをクリックします。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image14.png\" alt=\"\" width=\"600px\">\n<br />\n\n保存後、以下のように Webhook URL が表示されるため、これをコピーしておきます。\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image15.png\" alt=\"\" width=\"600px\">\n<br />\n\n以上で、Rocket.Chat側のGitLabからのWebHook受け入れ準備は完了です。\n\n<br />\n\n<a class=\"anchor\" id=\"anchor4\"></a>\n\n# GitLabにOutgoing WebHookを設定\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/webhook-step4.svg)\n\n<br />\n\nGitLabに管理者でログインし、プロジェクトの画面へ行き、「設定」→「Webhooks」をクリックします。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image16.png\" alt=\"\" width=\"600px\">\n<br />\n\n<blockquote class=\"warn\">\n<p>管理者エリア→システムフック にも似た設定がありますが、こちらは、GitLab全体に有効になります。仕様が異なりますので、「Rocket.ChatにIncoming WebHook(GitLab用)を設定」の手順で使用したScriptは正常に動作しません。</p>\n</blockquote>\n\n「URL」に「Rocket.ChatにIncoming WebHook(GitLab用)を設定」の手順でコピーしたURLを貼り付けます。  \n「Secret token」は空白で構いません。  \n「プッシュイベント」にチェックを入れます。  \n「SSL証明書検証の有効化」は、今回、Webhook URLが`http://`のため、チェックを外し、「Add webhook」をクリックします。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image17.png\" alt=\"\" width=\"600px\">\n<br />\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image18.png\" alt=\"\" width=\"600px\">\n<br />\n\n保存すると、以下のようにテストができるようになります。\n「テスト」→「Push events」をクリックしてみます。→前回のpush内容が Rocket.Chat、Slack ともに投稿されれば成功です。\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image19.png\" alt=\"\" width=\"600px\">\n<br />\n\n<br />\n\n・`Rocket.Chat`  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image21.png\" alt=\"\" width=\"400px\">\n<br />\n\n・`Slack`  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image20.png\" alt=\"\" width=\"400px\">\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/webhook-check1.svg)  \n\n<br />\n\n<blockquote class=\"alert\">\n<p><span style=\"color: red; \"><code>Hook execution failed: URL 'http://[ホスト名]/hooks/aaaaaaaaaaaaaaaaa/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' is blocked: Requests to the local network are not allowed</code></span></p>\n<p>のエラーになった場合、ローカルネットワークのWebHookを許可する設定が必要です。</p>\n<p><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/webhook-error1.png\" alt=\"\" width=\"600px\" style=\"margin-right: 10px\"></p>\n<p>この場合、以下のように設定します。</p>\n<br />\n<p>「管理者エリア」をクリックします。</p>\n<p><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/webhook-error2.png\" alt=\"\" width=\"600px\" style=\"margin-right: 10px\"></p>\n<br />\n<p>「設定」→「ネットワーク」 をクリックします。</p>\n<p><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/webhook-error3.png\" alt=\"\" width=\"400px\" style=\"margin-right: 10px\"></p>\n<br />\n<p>「web フックおよびサービスからのローカルネットワークへのリクエストを許可する。」にチェックを入れ、「Save changes」をクリックします。</p>\n<p><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/webhook-error4.png\" alt=\"\" width=\"600px\" style=\"margin-right: 10px\"></p>\n</blockquote>\n\n<br />\n\n<br />\n\n<a class=\"anchor\" id=\"anchor5\"></a>\n\n# Rocket.ChatにIncoming WebHook(Wekan用)を設定\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/webhook-step5.svg)  \n\n<br />\n\n管理者でログイン後、ユーザーのアイコンをクリックして、「管理」をクリックします。  \n「サービス連携」をクリックします。  \n「Incoming」を選択して、「+New」をクリックします。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image22.png\" alt=\"\" width=\"600px\">\n<br />\n\n「名前」は、\"Wekan\"とし、投稿先チャンネルは、\"#general\"とします。  \n「投稿ユーザー」は、Rocket.Chatで登録されているユーザー名を入力します。  \n「エイリアス」は、 \"wekan\" とします。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image23.png\" alt=\"\" width=\"600px\">\n<br />\n\nこの場合、投稿者の名前が以下のように表示されます。  \n\n<br />\n\n・`Rocket.Chat`  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image24.png\" alt=\"\" width=\"200px\">\n<br />\n\n・`Slack`  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image25.png\" alt=\"\" width=\"200px\">\n<br />\n\n絵文字は、アバターの画像になります。<a href=\"https://www.webfx.com/tools/emoji-cheat-sheet\" target=\"_blank\">https://www.webfx.com/tools/emoji-cheat-sheet/</a> 等で調べて、絵文字コードを入力します。今回 `:clipboard:` とします。Slackへはスクリプトによって、`:ghost:`になります。\n\n<br />\n\n今回、Scriptは登録しません。スクリプトを有効にするをオフのままにします。  \nスクリプトを使わない理由は、以下のようにWekanから、`content.text` が欲しい形で送られてくるからです。`content.text`は、そのままチャットのテキストに採用されます。  \n\n```javascript\n{\n  url: {\n    hash: null,\n    search: null,\n    query: {},\n    pathname: '/hooks/aaaaaaaaaaaaaaaaa/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n    path: '/hooks/aaaaaaaaaaaaaaaaa/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n  },\n  url_raw: '/hooks/aaaaaaaaaaaaaaaaa/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n  url_params: {\n    integrationId: 'aaaaaaaaaaaaaaaaa',\n    token: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n  },\n  content: {\n    text: 'foobar リスト \"リスト追加\" を ボード \"ボード作成\" に追加しました<br>' + 'http://wekan.itccorporation.jp/b/yyyyyyyyyyyyyyyyy/board',\n    listId: 'bbbbbbbbbbbbbbbbb',\n    boardId: 'yyyyyyyyyyyyyyyyy',\n    user: 'foobar',\n    description: 'act-createList'\n  },\n  content_raw: null,\n  headers: {\n    'content-type': 'application/json',\n    host: '192.168.11.6:3000',\n    'content-length': '265',\n    connection: 'close'\n  },\n  body: {\n    text: 'foobar リスト \"リスト追加\" を ボード \"ボード作成\" に追加しました<br>' + 'http://wekan.itccorporation.jp/b/yyyyyyyyyyyyyyyyy/board',\n    listId: 'bbbbbbbbbbbbbbbbb',\n    boardId: 'yyyyyyyyyyyyyyyyy',\n    user: 'foobar',\n    description: 'act-createList'\n  },\n  user: {\n    _id: 'zzzzzzzzzzzzzzzzz',\n    name: 'foobar',\n    username: 'foobar'\n  }\n}\n```\n\n「有効」スイッチをオンにして、保存ボタンをクリックします。\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image26.png\" alt=\"\" width=\"600px\">\n<br />\n\n保存後、以下のように Webhook URL が表示されるため、これをコピーしておきます。\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image27.png\" alt=\"\" width=\"600px\">\n<br />\n\n以上で、Rocket.Chat側のGitLabからのWebHook受け入れ準備は完了です。\n\n<br />\n\n<a class=\"anchor\" id=\"anchor6\"></a>\n\n# WekanにOutgoing WebHookを設定\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/webhook-step6.svg)\n\n<br />\n\nWekanに管理者でログインして、右上のアバターアイコンをクリック、「管理パネル」をクリックします。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image28.png\" alt=\"\" width=\"600px\">\n<br />\n\n「グローバルWebフック」をクリックして、  \n「Webフック名」は、\"Rocket.Chat\"として、  \n「URL」に「Rocket.ChatにIncoming WebHook(Wekan用)を設定」の手順でコピーしたURLを貼り付けます。  \nトークン(認証用オプション)は空白で構いません。  \n「発信Webフック」を選択して、「作成」をクリックします。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image29.png\" alt=\"\" width=\"400px\">\n<br />\n\nボードを追加してみます。\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image30.png\" alt=\"\" width=\"400px\">\n<br />\n\n・`Rocket.Chat`  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image31.png\" alt=\"\" width=\"400px\">\n<br />\n\n・`Slack`  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/image32.png\" alt=\"\" width=\"400px\">\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/webhook-check2.svg)\n\n<br />\n\nできました!\n\n<br />\n","description":"先日 ・GitLab ・Wekan ・Rocket.Chat をローカルの環境にインストールしました。 ※インストールについては、下記別記事を参照してください。 「Ubuntu 20.04.2.0にGitLabをインストール」 「CentOS8にwekanをインストール(ソースからビルド編)」 「CentOS8にRocket.Chatをインストール」 この環境は自宅や外出時のスマホなどのインターネットから直接見られません。 そこで、インターネットからチャットの内容、GitLab,Wekanのアクティビティについて確認できるようWebHookを利用して、以下の環境を構築しました。 Slackはインターネットのビジネスチャットサービスで、10000通まで無料です。(2021/06現在) Slackについては、WebHookの手順以外、今回解説しません。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"t62fw4_0c","createdAt":"2021-07-16T10:05:49.760Z","updatedAt":"2021-07-16T10:05:49.760Z","publishedAt":"2021-07-16T10:05:49.760Z","revisedAt":"2021-07-16T10:05:49.760Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/webhook/ITC_Engineering_Blog.png","alt":"GitLab,Wekan→Rocket.Chat→SlackのWebHook","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\nOKです!\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 とも呼ばれる)アーカイブは通常、すべての依存ライブラリを1つにまとめた単一の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 ビルドまで 1行で済む別のビルド方法があります。\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":"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>ビルド中に1回実行</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こちらは、「処理中」になり、数分後見たら、3つとも登録されていました。\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>(2週間くらい音沙汰無しのため、再度問い合わせ)「いつになったら返事してもらえる?」</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":"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`が有るのですが、こちらは、単純にキャッシュの有効期間です。デフォルトは5分です。  \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":"knfpjkzmc1","createdAt":"2021-05-08T09:45:31.035Z","updatedAt":"2023-11-09T10:08:40.460Z","publishedAt":"2021-05-08T09:45:31.035Z","revisedAt":"2023-11-09T10:08:40.460Z","title":"CentOS8にwekanをインストール(snap編)","category":{"id":"gg2a3kv3ofiu","createdAt":"2021-05-09T08:35:47.263Z","updatedAt":"2021-05-09T08:35:47.263Z","publishedAt":"2021-05-09T08:35:47.263Z","revisedAt":"2021-05-09T08:35:47.263Z","topics":"Wekan","logo":"/logos/Wekan.png","needs_title":false},"topics":[{"id":"gg2a3kv3ofiu","createdAt":"2021-05-09T08:35:47.263Z","updatedAt":"2021-05-09T08:35:47.263Z","publishedAt":"2021-05-09T08:35:47.263Z","revisedAt":"2021-05-09T08:35:47.263Z","topics":"Wekan","logo":"/logos/Wekan.png","needs_title":false},{"id":"n767cc7fin","createdAt":"2021-02-18T07:12:11.544Z","updatedAt":"2021-08-31T12:09:10.575Z","publishedAt":"2021-02-18T07:12:11.544Z","revisedAt":"2021-08-31T12:09:10.575Z","topics":"CentOS","logo":"/logos/CentOS.png","needs_title":false}],"content":"# はじめに\n  \nWekan Open-Source kanban  \nをsnapを使ってインストールしてみました。  \n<br />\nWekanは、OSSのかんばん管理ツールです。社内タスクの管理、見える化に役立ちます。  \nソースコードは、Node.js、Meteorフレームワークで構成されています。MITライセンスです。  \n同じ目的の場合、Trelloが有名ですが、閉じた環境で自力運用したくて、Wekanを選びました。  \n<br />\nインストール環境:CentOS Linux release 8.3.2011 (VMware上、インターネット接続あり)  \n\n<br />\n\n# snapインストール\n  \n<blockquote class=\"warn\">\n<p>root権限で作業していますので、全てsudoは省略しています。</p>\n</blockquote>\n\n<br />\n\nEPELリポジトリを追加します。  \n\n```shellsession\n# dnf install epel-release\nIs this ok [y/N]: y\n```\n※以降基本的にyのため、-yを付けます。  \n -y は、? [y/N]: のようなときに自動的に y とするオプションです。\n\n<br />\n\nパッケージを更新します。  \n\n```shellsession\n# sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*\n# dnf -y upgrade\n```\n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;\"><strong>【2023年11月更新】</strong></span></p>\n<p><code># sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*</code></p>\n<p>はブログ投稿当時無かった手順ですが、必要になりました。</p>\n</blockquote>\n\n<br />\n\nsnapdをインストールします。  \n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;\"><strong>【2023年11月更新】</strong></span></p>\n<p><code># dnf install -y snapd</code></p>\n<p>ではインストールできなくなっていることが分かりました。</p>\n<p>snapd インストール方法は、別記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/centos8-snapd\" target=\"_blank\">【CentOS8.3+snapd】ソースコードからrpmをビルド&SELinuxを調整でsnapdをインストール</a>」を参照してください。</p>\n</blockquote>\n\n```shellsession\n# dnf install -y snapd\n```\n<blockquote class=\"info\">\n<p>snapdは、パッケージ管理システムsnapのデーモン(常駐プログラム)です。パッケージ管理システムは、CentOSで言うと、yum、dnf。Ubuntuで言うと、aptみたいなものです。</p>\n<p>snapは、Linuxのディストリビューション(CentOS,Ubuntu,Rasbery pi,…)を気にしなくてアプリをインストールできる仕組みです。コンテナ技術でファイルシステムとは切り離されて隔離した環境下で動作しますので、既存環境を汚しません。アプリを削除するときも他のアプリの依存環境に影響しません。</p>\n<p>snapパッケージは、「Snap Store」と呼ばれる場所から配布されています。</p>\n</blockquote>\n\n<br />\n\nsnapd.socketをアクティベーションします。  \n(/usr/libexec/snapd/snapdを起動します。)  \n\n```shellsession\n# systemctl enable --now snapd.socket\n```\n\n<blockquote class=\"info\">\n<p>この時、snapdが起動します。snapdは常駐プログラムですので、snapコマンドで操作することになります。</p>\n</blockquote>\n\n<br />\n\n# wekanインストール\n  \nsnapコマンドで、Snap Storeからwekanをインストールします。\n\n```shellsession\n# snap install wekan\n```\n\n<blockquote class=\"warn\">\n<pre><code># systemctl enable --now snapd.socket</code></pre>\n<p>のすぐ後に実行すると、</p>\n<pre><code>error: too early for operation, device not yet seeded or device model not acknowledged</code></pre>\n<p>とエラーになります。数秒待って実行する必要があります。(再起動が確実です。)</p>\n</blockquote>\n\n<br />\n\nwekanの設定を行います。  \nhttp://wekan.itccorporation.jp:3001 でアクセスし、  \nメールサーバー:mailserver.example.com:587  \nメール送信ユーザー:user  \nメール送信パスワード:pass  \nとします。  \n\n```shellsession\n# snap set wekan root-url='http://wekan.itccorporation.jp'\n# snap set wekan port='3001'\n# snap set wekan mail-url=\"smtp://user:pass@mailserver.example.com:587\"\n```\n\n<br />\n\nwekanを再起動します。(設定の反映)\n\n```shellsession\n# systemctl restart snap.wekan.wekan\n```\n\n<br />\n\n# ブラウザアクセス\n  \nファイアウォール有りで、外から見る場合、3001ポートを開ける必要があります。\n\n```shellsession\n# firewall-cmd --zone=public --add-port=3001/tcp --permanent\n# firewall-cmd --reload\n```\n\n<br />\n\nhttp://wekan.itccorporation.jp:3001 へブラウザでアクセス  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/wekan_install/image01.png)  \n\n<br />\n\nできました!\n\n","description":"Wekan Open-Source kanban をsnapを使ってインストールしてみました。 Wekanは、OSSのかんばん管理ツールです。社内タスクの管理、見える化に役立ちます。 ソースコードは、Node.js、Meteorフレームワークで構成されています。MITライセンスです。 同じ目的の場合、Trelloが有名ですが、閉じた環境で自力運用したくて、Wekanを選びました。 インストール環境:CentOS Linux release 8.3.2011 (VMware上、インターネット接続あり) snapインストール EPELリポジトリを追加します。 # dnf install epel-release Is this ok [y/N]: y パッケージを更新します。 # dnf -y upgrade","reflect_updatedAt":true,"reflect_revisedAt":true,"seo_images":[{"id":"ss_cpc8w6","createdAt":"2021-07-16T10:06:02.739Z","updatedAt":"2023-11-11T11:06:05.057Z","publishedAt":"2021-07-16T10:06:02.739Z","revisedAt":"2023-11-11T11:06:05.057Z","url":"https://itc-engineering-blog.imgix.net/wekan-install/ITC_Engineering_Blog1.png","alt":"CentOS8にwekanをインストール(snap編)","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今回、全て1台の 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」をクリック2\" 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 のため、ここまで来たら、OKとします。\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":"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(いろいろバンドルされたもののこと)の1つのコンポーネントを再起動するには、<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 実行体)は、以下の3種類ありますが、今回は、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 ログイン1\" 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 ログイン2\" 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 インストールOK!!\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へpush1\" 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へpush2\" 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へpush3\" 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\nGood!!\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 アドレス、証明書関係で対応漏れが無ければOKです。\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手動起動1\" 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手動起動2\" 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手動起動3\" 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手動起動4\" 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":"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がカレントフォルダに置かれればOK  \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":"gitlab-openresty-php","createdAt":"2022-01-05T09:15:48.329Z","updatedAt":"2022-05-19T11:05:32.611Z","publishedAt":"2022-01-05T09:15:48.329Z","revisedAt":"2022-05-19T11:05:32.611Z","title":"GitLabバンドルnginx廃止→OpenResty - GitLab - 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\nGitLab バンドルの nginx は、以下の GitLab 構造図を見ると、80,443 ポートの窓口の役割になっています。<span style=\"color: red;\"><strong>今回、これを OpenResty に変更します。</strong></span>\n\n<a href=\"https://docs.gitlab.com/ee/development/img/architecture_simplified_v14_9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://docs.gitlab.com/ee/development/img/architecture_simplified_v14_9.png\" alt=\"GitLab 構造図\"></a>\n\n<br />\n\n【変更前】  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/nginx.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/nginx.png\" alt=\"GitLab 構造図 右上部分\" width=\"330\" height=\"331\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n↓\n\n<br />\n\n【変更後】  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/OpenResty.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/OpenResty.png\" alt=\"GitLab 構造図 右上部分 最終版\" width=\"641\" height=\"331\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n「<a href=\"https://itc-engineering-blog.netlify.app/blogs/gitlab-nginx-php\" target=\"_blank\">GitLab バンドル nginx を利用して php の独自 Web アプリを同居させる手順</a>」では、GitLab バンドル nginx をそのまま利用して、Web アプリを同居させましたが、今回は、<span style=\"color: red;\"><strong>GitLab バンドル nginx を OpenResty に変更して、GitLab と Web アプリを同居させます。</strong></span>(今回の記事中のゴールは、Web アプリ起動= phpinfo 表示です。)\n\n<blockquote class=\"warn\">\n<p>【GitLab環境前提】</p>\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>【OpenResty, php環境前提】</p>\n<p>【GitLab環境前提】に加えて、GitLabインストール済み環境へ「<a href=\"https://itc-engineering-blog.netlify.app/blogs/lua-resty-openidc\" target=\"_blank\">OpenResty lua-resty-openidc phpでSSO Webアプリ環境作成</a>」でOpenRestyとphpがインストールされた環境からスタートが前提です。(<code>http://webapp.example.com/info.php</code>を確認するところまでやったものとします。)</p>\n<p><span style=\"color: red;\"><strong>また、\"そもそもOpenRestyとは何か?\"の部分も上記記事に記載がありますので、ここでは解説しません。Nginxのことと読み替えても支障は無いと思います。</strong></span></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(GitLabバンドル版の方→廃止)</code></p>\n<p> <code>nginx openresty/1.19.9.1</code></p>\n<p> <code>PHP 8.0.14</code></p>\n</blockquote>\n\n<br />\n\n# バンドル nginx 廃止\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/nginx2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/nginx2.png\" alt=\"バンドルnginx廃止 図\" width=\"551\" height=\"331\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<a href=\"https://docs.gitlab.com/omnibus/settings/nginx.html#using-a-non-bundled-web-server\" target=\"_blank\">GitLab 公式マニュアル「Using a non-bundled web-server」</a>を参考に、GitLab からバンドルされた nginx を切り離して、OpenResty を使う設定にします。\n\n```shellsession\n# vi /etc/gitlab/gitlab.rb\n```\n\n```sh\n# nginx['enable'] = true\n↓\nnginx['enable'] = false\n```\n\n・・・これだけになります。  \nつまり、GitLab 公式マニュアルの「1. Disable bundled NGINX」しか必要なかったです。\n\n反映させるために、以下を実行します。これにより、GitLab - GitLab バンドル nginx は切り離されます。\n\n```shellsession\n# gitlab-ctl stop nginx\n# gitlab-ctl reconfigure\n```\n\n以下、必要なかった他の手順について、書きます。\n\n<br />\n\n**2. Set the username of the non-bundled web-server user**\n\n```sh\n# web_server['external_users'] = []\n↓\nweb_server['external_users'] = ['www-data']\n```\n\n差し替える Web サーバーのユーザーに合わせる必要がありますが、今回の手順では、Web サーバー= OpenResty のユーザーの方を逆に GitLab のユーザーに合わせて `gitlab-www` にするため、何も設定しません。\n\n<br />\n\n**3. Add the non-bundled web-server to the list of trusted proxies**\n\n```sh\n# gitlab_rails['trusted_proxies'] = []\n↓\ngitlab_rails['trusted_proxies'] = [ '192.168.1.0/24', '192.168.2.1', '2001:0db8::/32' ]\n```\n\n差し替える Web サーバーの IP アドレスが GitLab と異なる場合、設定しないといけないようですが、今回、OpenResty が GitLab と同居するパターンのため、何も設定しません。\n\n<blockquote class=\"warn\">\n<p>差し替えるWebサーバーがDockerなどの時に、必要なようですが、そういったパターンでは、試していません。</p>\n</blockquote>\n\n<br />\n\n**4.(Optional) Set the right GitLab Workhorse settings if using Apache**\n\n```sh\n# gitlab_workhorse['listen_network'] = \"unix\"\n# gitlab_workhorse['listen_addr'] = \"/var/opt/gitlab/gitlab-workhorse/sockets/socket\"\n↓\ngitlab_workhorse['listen_network'] = \"tcp\"\ngitlab_workhorse['listen_addr'] = \"127.0.0.1:8181\"\n```\n\nUNIX ソケットをサポートされていない Apache の場合、TCP 通信に切り替えるために必要なようですが、今回は、OpenResty(nginx)のため、UNIX ソケットをそのまま利用し、何も設定しません。\n\n<br />\n\n**5.Download the right web server configs**  \n<span style=\"color: red;\">この手順は必要ですが、`gitlab.rb`の書き換えではないため、後ほどの説明になります。</span>\n\n<br />\n\n# パーミッション調整\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/nginx3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/nginx3.png\" alt=\"パーミッション調整 図\" width=\"581\" height=\"331\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\nOpenResty、php-fpm のパーミッション設定を GitLab ユーザー `gitlab-www` に変更します。\n\n```shellsession\n# vi /usr/local/openresty/nginx/conf/nginx.conf\n```\n\n```nginx\nuser gitlab-www;\n```\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と\nlisten.owner = gitlab-www\nlisten.group = gitlab-www\n```\n\n<blockquote class=\"info\">\n<p>前回記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/lua-resty-openidc\" target=\"_blank\">OpenResty lua-resty-openidc phpでSSO Webアプリ環境作成</a>」に沿って、Webアプリは、<code>/opt/webapp/www/html/info.php</code>にあるものとします。</p>\n</blockquote>\n\n```shellsession\n# chown -R gitlab-www: /opt/webapp\n```\n\n<br />\n\n# OpenResty 設定\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/nginx4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/nginx4.png\" alt=\"OpenResty設定 図\" width=\"521\" height=\"331\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\nマニュアルの「GitLab recipes repository」部分がリンクになっていて、こちらから GitLab 用コンフィグファイルをダウンロードします。\n\n<a href=\"https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server\" target=\"_blank\">`https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server`</a>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/image3.png\" alt=\"GitLab用コンフィグファイルをダウンロード1\" width=\"1223\" height=\"582\" loading=\"lazy\"></a>\n\n<br />\n\nnginx をクリックします。\n\n<a href=\"https://gitlab.com/gitlab-org/gitlab-recipes/-/tree/master/web-server/nginx\" target=\"_blank\">`https://gitlab.com/gitlab-org/gitlab-recipes/-/tree/master/web-server/nginx`</a>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/image4.png\" alt=\"GitLab用コンフィグファイルをダウンロード2\" width=\"1223\" height=\"554\" loading=\"lazy\"></a>\n\n<br />\n\n「Nginx config moved to official repository」となっていて、「移動した」と言っています。  \n移動先リンクをクリックします。  \n<a href=\"https://gitlab.com/gitlab-org/gitlab-foss/-/tree/master/lib/support/nginx\" target=\"_blank\">`https://gitlab.com/gitlab-org/gitlab-foss/-/tree/master/lib/support/nginx`</a>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/image5.png\" alt=\"GitLab用コンフィグファイルをダウンロード3\" width=\"1222\" height=\"676\" loading=\"lazy\"></a>\n\n<br />\n\ngitlab-ssl  をダウンロードします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/image6.png\" alt=\"GitLab用コンフィグファイルをダウンロード4\" width=\"1223\" height=\"617\" loading=\"lazy\"></a>\n\n<blockquote class=\"warn\">\n<p>今回、<code>https://</code>でGitLabのソースコード管理機能に接続するため、このファイルのみになります。<code>http://</code>接続や、GitLab Pages、GitLab Registry機能の利用は想定していません。</p>\n</blockquote>\n\n<br />\n\nダウンロードしたファイルを`/usr/local/openresty/nginx/conf/gitlab-ssl.conf`として配置し、編集します。\n\n```shellsession\n# vi /usr/local/openresty/nginx/conf/gitlab-ssl.conf\n```\n\nここで、変えるべきところは、以下のような部分になると思います。\n\n<br />\n\n**・UNIX ドメインソケット**\n\n```nginx\nupstream gitlab-workhorse {\n  # GitLab socket file,\n  # for Omnibus this would be: unix:/var/opt/gitlab/gitlab-workhorse/sockets/socket\n  server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0;\n}\n↓\nupstream gitlab-workhorse {\n  # GitLab socket file,\n  # for Omnibus this would be: unix:/var/opt/gitlab/gitlab-workhorse/sockets/socket\n  #server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0;\n  server unix:/var/opt/gitlab/gitlab-workhorse/sockets/socket fail_timeout=0;\n}\n```\n\nこれは、`/etc/gitlab/gitlab.rb`に\n\n```sh\n# gitlab_workhorse['listen_addr'] = \"/var/opt/gitlab/gitlab-workhorse/sockets/socket\"\n```\n\nが書かれているからです。(デフォルトの UNIX ソケット位置)\n\n<br />\n\n**・GitLab サーバー名**\n\n```nginx\n  server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com\n↓\n  server_name gitlab-test.itccorporation.jp; ## Replace this with something like gitlab.example.com\n```\n\nGitLab のサーバー名に書き換えます。(2箇所あります。)\n\n<br />\n\n**・GitLab ログパス**\n\n```nginx\n  access_log  /var/log/nginx/gitlab_access.log gitlab_ssl_access;\n  error_log   /var/log/nginx/gitlab_error.log;\n↓\n  access_log  /var/log/gitlab/nginx/gitlab_access.log gitlab_ssl_access;\n  error_log   /var/log/gitlab/nginx/gitlab_error.log;\n```\n\nどこでも良いのですが、バンドル版 nginx が上記パスだったため、合わせます。(2箇所あります。)\n\n<br />\n\n**・SSL 証明書パス**\n\n```nginx\n  ssl_certificate /etc/nginx/ssl/gitlab.crt;\n  ssl_certificate_key /etc/nginx/ssl/gitlab.key;\n↓\n  ssl_certificate /etc/pki/tls/certs/ca.crt;\n  ssl_certificate_key /etc/pki/tls/private/ca.key;\n```\n\n設置した SSL 証明書とキーのパスに合わせます。\n\n<br />\n\n# 動作確認\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/nginx5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/nginx5.png\" alt=\"include 図\" width=\"521\" height=\"331\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n`/usr/local/openresty/nginx/conf/gitlab-ssl.conf`を OpenResty 本体側の設定からインクルードします。\n\n```shellsession\n# vi /usr/local/openresty/nginx/conf/nginx.conf\n```\n\n```nginx\n    #}\ninclude gitlab-ssl.conf;\ninclude webapp.conf;\n}\n```\n\n末尾辺りに`include gitlab-ssl.conf;`を記入します。なお、`include webapp.conf;`は既にインストール済みの Web アプリの設定とします。\n\nOpenResty を再起動します。\n\n```shellsession\n# systemctl restart openresty\n```\n\n<br />\n\nGitLab と Web アプリは、同じ IP アドレスに名前解決できるものとします。\n\n<br />\n\nそれぞれにアクセスしてみます。\n\n・`https://gitlab-test.itccorporation.jp/`(GitLab)\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/image1.png\" alt=\"GitLab アクセス\" width=\"974\" height=\"501\" loading=\"lazy\"></a>\n\n<br />\n\n・`http://webapp.example.com/`(Web アプリ)\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/image2.png\" alt=\"GitLab アクセス\" width=\"1012\" height=\"550\" loading=\"lazy\"></a>\n\n<br />\n\nヨシ!\n","description":"GitLab バンドル nginx を別のWebサーバー (OpenResty(nginx)) に変更して、GitLab と Web アプリを同居させる手順をまとめました。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"81lycuv89v","createdAt":"2022-01-05T09:12:45.659Z","updatedAt":"2022-01-05T09:12:45.659Z","publishedAt":"2022-01-05T09:12:45.659Z","revisedAt":"2022-01-05T09:12:45.659Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-openresty-php/ITC_Engineering_Blog.png","alt":"GitLabバンドルnginx廃止→OpenResty - GitLab - Webアプリ同居手順","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` のままだと1行が長すぎてリンターに引っかかっていますので、修正します。\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=\"1行が長すぎてリンターエラー 修正前\" 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=\"1行が長すぎてリンターエラー 修正後\" 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起動1\" 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起動2\" 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=\"フロントエンド バックエンド ブレークポイント1\" 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=\"フロントエンド バックエンド ブレークポイント2\" 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=\"フロントエンド バックエンド ブレークポイント3\" 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":"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 1\" 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 2\" 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 3\" 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":"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今回、全て1台の 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 を作成します。(値は適当でOKです。)\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\",` により、メッセージが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により、1件処理されたことが分かります。\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 をチェックするときに、エラーになりました。(DBを何も指定しないと、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` の1行を追加します。\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**原因1:**  \nパスを通していない。\n\n<br />\n\n**対処内容1:**  \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**原因2:**  \nruby のバージョンを指定していない。\n\n<br />\n\n**対処内容2:**  \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":"go-oidc-azuread","createdAt":"2023-12-30T11:35:55.944Z","updatedAt":"2023-12-30T11:35:55.944Z","publishedAt":"2023-12-30T11:35:55.944Z","revisedAt":"2023-12-30T11:35:55.944Z","title":"go-oidcを使ってGo言語アプリケーション実装 Azure AD(Microsoft Entra ID)認証","category":{"id":"9wvhex1nhpyg","createdAt":"2023-12-29T12:09:49.231Z","updatedAt":"2023-12-29T12:09:49.231Z","publishedAt":"2023-12-29T12:09:49.231Z","revisedAt":"2023-12-29T12:09:49.231Z","topics":"Go","logo":"/logos/Go.png","needs_title":false},"topics":[{"id":"9wvhex1nhpyg","createdAt":"2023-12-29T12:09:49.231Z","updatedAt":"2023-12-29T12:09:49.231Z","publishedAt":"2023-12-29T12:09:49.231Z","revisedAt":"2023-12-29T12:09:49.231Z","topics":"Go","logo":"/logos/Go.png","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":"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}],"content":"# はじめに\n\ngo-oidc(<a href=\"https://github.com/coreos/go-oidc\" target=\"_blank\">https://github.com/coreos/go-oidc</a>)を使って、Go 言語アプリケーション実装 ~ Azure AD(Microsoft Entra ID) を使った OpenID Connect 認証を行ってみました。  \n基本的には、go-oidc/example/userinfo/app.go というのを見つけて、ID とか書き換えるだけで終了しました。  \napp.go は、ログイン後、アクセストークンを使って、`https://graph.microsoft.com/oidc/userinfo` からログインユーザ自身の情報を取得し、表示しています。  \n今回、Go のインストールから全手順の紹介になります。\n\n<a href=\"https://itc-engineering-blog.imgix.net/go-oidc-azuread/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/go-oidc-azuread/image1.png\" alt=\"OpenID Connect関係図\" width=\"1111\" height=\"392\" loading=\"lazy\"></a>\n\n<br />\n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;\"><strong>本記事情報の設定不足、誤りにより何らかの問題が生じても、一切責任を負いません。</strong></span></p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>標準的な手順で Ubuntu 22.04.3 LTS インストールしたての状況からスタートです。</p>\n</blockquote>\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>Go 1.18.1</p>\n</blockquote>\n\n<br />\n\n# golang-go インストール\n\nGo 言語をインストールします。\n\n```shellsession\n$ sudo apt update -y\n$ sudo apt install golang-go -y\n$ go version\ngo version go1.18.1 linux/amd64\n```\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**名前**: `go-oidc-azure-example`(任意です。)  \n**サポートされているアカウントの種類**:`この組織ディレクトリのみに含まれるアカウント (<テナント名> のみ - シングル テナント)`  \n**リダイレクト URI (省略可能)**:`Web` `http://localhost:5556/auth/callback`\n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;\"><strong>リダイレクト URI については、何でも良いですが、この後、アプリの実装の方で合わせます。</strong></span></p>\n<p><span style=\"color: red;\"><strong>アプリの実装とここで設定した URI がずれている場合、認証が通りません。</strong></span></p>\n<p><span style=\"color: red;\"><strong>なお、<code>http://</code> が許されるのは、 <code>localhost</code> だけです。</strong></span></p>\n</blockquote>\n\n<a href=\"https://itc-engineering-blog.imgix.net/go-oidc-azuread/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/go-oidc-azuread/image2.png\" alt=\"アプリ情報を入力\" width=\"1072\" height=\"820\" loading=\"lazy\"></a>\n\n<br />\n\n**概要** をクリックして、  \n**アプリケーション (クライアント) ID** から clientID(後で行うアプリ側設定項目)を確認しておきます。\n\n<a href=\"https://itc-engineering-blog.imgix.net/go-oidc-azuread/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/go-oidc-azuread/image3.png\" alt=\"概要 アプリケーション (クライアント) ID\" width=\"1072\" height=\"455\" loading=\"lazy\"></a>\n\n<br />\n\n**証明書とシークレット** をクリックし、**+新しいクライアント シークレット** をクリックします。\n<a href=\"https://itc-engineering-blog.imgix.net/go-oidc-azuread/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/go-oidc-azuread/image4.png\" alt=\"証明書とシークレット +新しいクライアント シークレット\" width=\"1073\" height=\"563\" loading=\"lazy\"></a>\n\n<br />\n\n**説明**、**有効期限** を任意の値に設定し、**追加** をクリックします。\n<a href=\"https://itc-engineering-blog.imgix.net/go-oidc-azuread/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/go-oidc-azuread/image5.png\" alt=\"説明 有効期限\" width=\"1073\" height=\"600\" loading=\"lazy\"></a>\n\n<br />\n\n<span style=\"color: red;\"><strong>ここで出てくる **値** の文字列が clientSecret(後で行うアプリ側設定項目)の文字列になります。(シークレット ID の方ではありません。)</strong></span>  \n<span style=\"color: red;\"><strong>二度と表示されないため、ここで、メモっておきます。</strong></span>\n\n<a href=\"https://itc-engineering-blog.imgix.net/go-oidc-azuread/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/go-oidc-azuread/image6.png\" alt=\"値=clientSecret(後で行うアプリ側設定項目)\" width=\"1147\" height=\"625\" loading=\"lazy\"></a>\n\n<br />\n\n# アプリケーション実装\n\nアプリケーションを実装します。\n\n<br />\n\nアプリケーションは以下の仕様とします。  \n**URL**:`http://localhost:5556`  \n**表示内容**:`<https://graph.microsoft.com/oidc/userinfo レスポンスのJSON>`  \n\n<br />\n\n```shellsession\n$ cd ~/\n$ mkdir go-oidc-azure-example\n$ cd go-oidc-azure-example\n$ vi app.go\n```\n\n```go:go-oidc-azure-example/app.go\npackage main // このコードがmainパッケージに属していることを示します。\n\nimport (\n\t\"crypto/rand\"     // 暗号学的に安全な乱数を生成するためのパッケージ\n\t\"encoding/base64\" // base64エンコーディングとデコーディングを行うためのパッケージ\n\t\"encoding/json\"   // JSONエンコーディングとデコーディングを行うためのパッケージ\n\t\"io\"              // 入出力操作を行うためのパッケージ\n\t\"log\"             // ログメッセージを出力するためのパッケージ\n\t\"net/http\"        // HTTPクライアントとサーバーの実装を提供するパッケージ\n\t\"time\"            // 時間を扱うための関数や型を提供するパッケージ\n\n\t\"github.com/coreos/go-oidc/v3/oidc\" // OpenID Connectクライアントを作成するためのパッケージ\n\t\"golang.org/x/net/context\"          // コンテキストを管理するためのパッケージ\n\t\"golang.org/x/oauth2\"               // OAuth2クライアントを作成するためのパッケージ\n)\n\nvar (\n\t// clientID     = os.Getenv(\"GOOGLE_OAUTH2_CLIENT_ID\")\n\t// clientSecret = os.Getenv(\"GOOGLE_OAUTH2_CLIENT_SECRET\")\n\tclientID     = \"<アプリケーション (クライアント) ID>\"     // OAuth2クライアントID\n\tclientSecret = \"<証明書とシークレットの「値」>\" // OAuth2クライアントシークレット\n)\n\nfunc randString(nByte int) (string, error) { // nByte長のランダムな文字列を生成する関数\n\tb := make([]byte, nByte)\n\tif _, err := io.ReadFull(rand.Reader, b); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn base64.RawURLEncoding.EncodeToString(b), nil\n}\n\nfunc setCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) {\n\tc := &http.Cookie{\n\t\tName:     name,\n\t\tValue:    value,\n\t\tMaxAge:   int(time.Hour.Seconds()),\n\t\tSecure:   r.TLS != nil,\n\t\tHttpOnly: true,\n\t\t// Path:     \"/\",\n\t}\n\thttp.SetCookie(w, c) // クッキーを設定\n\t// クッキーにより、リクエスト時のstateを保持して、リダイレクトURLのstateとの一致を後で確認できます。\n\t// 例: Set-Cookie: state=1L7bJe0tMcF7xEDxTrLDnA; Max-Age=3600; HttpOnly\n}\n\nfunc main() { // メイン関数\n\tctx := context.Background() // 新しい背景コンテキストを作成\n\n\t// provider, err := oidc.NewProvider(ctx, \"https://accounts.google.com\")\n\tprovider, err := oidc.NewProvider(ctx, \"https://login.microsoftonline.com/<ディレクトリ (テナント) ID>/v2.0\") // 新しいOpenID Connectプロバイダーを作成\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tconfig := oauth2.Config{ // OAuth2クライアントの設定を作成\n\t\tClientID:     clientID,\n\t\tClientSecret: clientSecret,\n\t\tEndpoint:     provider.Endpoint(),\n\t\t// RedirectURL:  \"http://127.0.0.1:5556/auth/google/callback\",\n\t\tRedirectURL: \"http://localhost:5556/auth/callback\",// Azure AD アプリの登録で設定した リダイレクト URI←一致している必要あり\n\t\tScopes:      []string{oidc.ScopeOpenID, \"profile\", \"email\"},\n\t}\n\n\thttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // ルートパスに対するハンドラを登録\n\t\tstate, err := randString(16)\n\t\tif err != nil {\n\t\t\thttp.Error(w, \"Internal error\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tsetCallbackCookie(w, r, \"state\", state)\n\n\t\thttp.Redirect(w, r, config.AuthCodeURL(state), http.StatusFound) // ユーザーをOAuth2プロバイダーの認証ページにリダイレクト\n\t})\n\n\t// http.HandleFunc(\"/auth/google/callback\", func(w http.ResponseWriter, r *http.Request) {\n\thttp.HandleFunc(\"/auth/callback\", func(w http.ResponseWriter, r *http.Request) { // 認証コールバックに対するハンドラを登録\n\t\t// リクエスト時state(クッキー)とコールバックURLのstateと一致しするか確認します。\n\t\tstate, err := r.Cookie(\"state\") // Cookieからstateパラメータを取得\n\t\tif err != nil {\n\t\t\thttp.Error(w, \"state not found\", http.StatusBadRequest) // stateパラメータが見つからない場合はエラーを返す\n\t\t\treturn\n\t\t}\n\t\t// ここで、アプリケーションは以前に送信したstateパラメータと一致するか確認します。\n\t\tif r.URL.Query().Get(\"state\") != state.Value {\n\t\t\thttp.Error(w, \"state did not match\", http.StatusBadRequest) // stateパラメータが一致しない場合はエラーを返す\n\t\t\treturn\n\t\t}\n\n\t\toauth2Token, err := config.Exchange(ctx, r.URL.Query().Get(\"code\")) // 認証コードをアクセストークンに交換\n\t\tif err != nil {\n\t\t\thttp.Error(w, \"Failed to exchange token: \"+err.Error(), http.StatusInternalServerError) // トークンの交換に失敗した場合はエラーを返す\n\t\t\treturn\n\t\t}\n\n\t\tuserInfo, err := provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token)) // ユーザー情報を取得\n\t\tif err != nil {\n\t\t\thttp.Error(w, \"Failed to get userinfo: \"+err.Error(), http.StatusInternalServerError) // ユーザー情報の取得に失敗した場合はエラーを返す\n\t\t\treturn\n\t\t}\n\n\t\tresp := struct { // レスポンスを作成\n\t\t\tOAuth2Token *oauth2.Token\n\t\t\tUserInfo    *oidc.UserInfo\n\t\t}{oauth2Token, userInfo}\n\t\tdata, err := json.MarshalIndent(resp, \"\", \"    \") // レスポンスをJSONに変換\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError) // JSONの変換に失敗した場合はエラーを返す\n\t\t\treturn\n\t\t}\n\t\tw.Write(data) // レスポンスを書き込む\n\t})\n\n\tlog.Printf(\"listening on http://%s/\", \"127.0.0.1:5556\") // サーバーがリッスンしているアドレスをログに出力\n\tlog.Fatal(http.ListenAndServe(\"127.0.0.1:5556\", nil))   // HTTPサーバーを起動\n}\n```\n\n<br />\n\n# 変更点\n\ngo-oidc/example/userinfo/app.go から変えた点は、以下です。\n\n<br />\n\n**・clientID、clientSecret 直書き**  \n**・OpenID Connect プロバイダー URL 変更**  \n**・callback URL 変更**  \n**・コメントによる説明書き追加**\n\n<br />\n\n# state について\n\n<span style=\"color: red;\">`state` についてですが、クロスサイトリクエストフォージェリ(CSRF)を防ぐために、自分でチェックしています。</span>  \n手順としては、以下です。  \n\n<br />\n\n`http://localhost:5556/` にアクセス  \n↓  \nクッキーセット  \n例:`Set-Cookie: state=1L7bJe0tMcF7xEDxTrLDnA; Max-Age=3600; HttpOnly`  \n↓  \n認証ページにリダイレクト  \n↓  \n認証成功  \n↓  \n`http://localhost:5556/auth/callback` にリダイレクト(自動的にアクセス)  \n↓  \nクッキーの `state` とリダイレクト URL(例:`http://localhost:5556/auth/callback?...&state=1L7bJe0tMcF7xEDxTrLDnA&...`)との `state` の一致を確認\n\n<br />\n\n<blockquote class=\"warn\">\n<p>この実装を行わないと、クロスサイトリクエストフォージェリ(CSRF)脆弱性ありのアプリになります。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>余談ですが、Cookie を使うため、<code>http://127.0.0.1:5556</code> でアクセス → 認証成功 → <code>http://localhost:5556</code> でリダイレクトとした場合、URL が異なるとみなされるため、\"state not found\" になります。<code>http://localhost:5556</code> でアクセスが必要です。これで小一時間ハマりました...。</p>\n</blockquote>\n\n<br />\n\n# 実行\n\n実行します。\n\n```shellsession\n$ go mod init example.com/go-oidc-azure-example\n$ go mod tidy\n$ go run app.go\n2023/12/29 22:06:13 listening on http://127.0.0.1:5556/\n```\n\n<br />\n\nブラウザで `http://localhost:5556/` にアクセスします。\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/go-oidc-azuread/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/go-oidc-azuread/image7.png\" alt=\"login.microsoftonline.com サインイン\" width=\"993\" height=\"699\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/go-oidc-azuread/image8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/go-oidc-azuread/image8.png\" alt=\"login.microsoftonline.com パスワードの入力\" width=\"879\" height=\"641\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/go-oidc-azuread/image9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/go-oidc-azuread/image9.png\" alt=\"login.microsoftonline.com 要求されているアクセス許可\" width=\"879\" height=\"643\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/go-oidc-azuread/image10.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/go-oidc-azuread/image10.png\" alt=\"graph.microsoft.com userinfoレスポンス結果\" width=\"1207\" height=\"770\" loading=\"lazy\"></a>\n\n<br />\n\nヨシっ!\n","description":"go-oidcを使って、Go 言語アプリケーション実装~Azure AD(Microsoft Entra ID)を使ったOpenID Connect認証を行ってみました。その全手順とソースコードです。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"up96blc-9m","createdAt":"2023-12-30T11:34:39.563Z","updatedAt":"2023-12-30T11:34:39.563Z","publishedAt":"2023-12-30T11:34:39.563Z","revisedAt":"2023-12-30T11:34:39.563Z","url":"https://itc-engineering-blog.imgix.net/go-oidc-azuread/ITC_Engineering_Blog.png","alt":"go-oidcを使ってGo言語アプリケーション実装 Azure AD(Microsoft Entra ID)認証","width":1200,"height":630}],"seo_authors":[]},{"id":"gitlab-download-app","createdAt":"2021-12-15T12:25:16.248Z","updatedAt":"2022-01-03T11:46:29.641Z","publishedAt":"2021-12-15T12:25:16.248Z","revisedAt":"2022-01-03T11:46:29.641Z","title":"GitLab APIを利用した一括ダウンロード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":"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":"6iqnys2ew","createdAt":"2021-02-18T07:12:50.055Z","updatedAt":"2021-08-31T12:09:00.626Z","publishedAt":"2021-02-18T07:12:50.055Z","revisedAt":"2021-08-31T12:09:00.626Z","topics":"jQuery","logo":"/logos/jQuery.png","needs_title":false},{"id":"o9t1ulsh2g","createdAt":"2021-07-03T13:32:00.531Z","updatedAt":"2021-08-31T12:03:24.477Z","publishedAt":"2021-07-03T13:32:00.531Z","revisedAt":"2021-08-31T12:03:24.477Z","topics":"JavaScript","logo":"/logos/JavaScript.png","needs_title":false},{"id":"o8o4z1zp7","createdAt":"2021-09-04T13:13:59.381Z","updatedAt":"2021-09-04T13:13:59.381Z","publishedAt":"2021-09-04T13:13:59.381Z","revisedAt":"2021-09-04T13:13:59.381Z","topics":"HTML","logo":"/logos/HTML5.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":"2ijwygxyjj","createdAt":"2021-06-19T10:12:08.920Z","updatedAt":"2021-08-31T12:03:42.373Z","publishedAt":"2021-06-19T10:12:08.920Z","revisedAt":"2021-08-31T12:03:42.373Z","topics":"WebHook","logo":"/logos/WebHook.png","needs_title":false}],"content":"# はじめに\n\nGitLab API を利用した一括ダウンロードWebアプリを php で作りました。GitHub に公開しています。→ リポジトリ:<a href=\"https://github.com/itc-lab/gitlab-download-app\" target=\"_blank\">itc-lab / gitlab-download-app</a>  \n「<a href=\"https://chrome.google.com/webstore/detail/developers-download-helpe/apchbjkblfhmkohghpnhidldebmpmjnn?hl=ja\" target=\"_blank\">Developer's Download Helper</a>」という拡張機能があるのは知っているのですが、過去にさかのぼってダウンロードしたり、.tar.gz でダウンロードしたかったり、他にもこういう仕様であって欲しいというところが出てきて、いっそのこと作ることにしました。  \n...一番大きいモチベーション → 弊社では、`Araxis Merge`というソフトで diff を見てマージするということをよくやっています。一度ダウンロードして、現在、1つ前、2つ前の3者間比較も良くやりますので、素早くダウンロードして比較できるのは助かります。  \n\n<br />\n\n以降、<a href=\"https://github.com/itc-lab/gitlab-download-app\" target=\"_blank\">itc-lab / gitlab-download-app</a>のことを「GitLab-Download-App」と称します。\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/gitlab-download-app1.gif\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/gitlab-download-app1.gif\" alt=\"「GitLab-Download-App」\" width=\"500\" height=\"190\" loading=\"lazy\"></a>\n\n<br />\n\n**【特徴】**  \ngit コマンドではなく、GUI で以下の事が全てできます。(git の知識は必要ありません。)  \n・グループ/プロジェクトの全ブランチ、タグ名、Commit 日時を一覧表示できる。  \n・過去の commit をダウンロードできる。  \n・.tar.gz または .zip でまとめてダウンロードできる。  \n・1つ前の commit との差分だけダウンロードができる。  \n・タグを選択してダウンロードができる。  \n・commit の内容(メッセージ)を素早く確認できる。  \n・アーカイブファイルの owner:group を指定してダウンロードできる(.tar.gz の場合)\n\n<br />\n\n<blockquote class=\"alert\">\n<p>今後の課題ですが、アカウントによる権限制御は、今のところ仕様にありません。トークンを取得した人の権限で一覧表示、ダウンロードできます。</p>\n</blockquote>\n\n<blockquote class=\"alert\">\n<p>GitLab-Download-Appは、MITライセンスです。自由に使用、改変していただいても構いませんが、GitLab-Download-Appの使用により、何らかの問題が生じても、一切責任を負いません。</p>\n</blockquote>\n\n<blockquote class=\"alert\">\n<p>別記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/fgm-dkbu10\" target=\"_blank\">Ubuntu 20.04.2.0にGitLabをインストール</a>」のようなオンプレミスのGitLabを想定しています。クラウド版GitLabは考慮外です。</p>\n</blockquote>\n\n<br />\n\n# 構成\n\n構成は以下です。  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/GitLabDownloadApp1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/GitLabDownloadApp1.png\" alt=\"GitLab-Download-App 構成図\" width=\"816\" height=\"220\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\nWeb サーバーは何でも良く、例えば、GitLab が使っている Nginx を利用して、以下のように GitLab 同居でも構いません。  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/GitLabDownloadApp2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/GitLabDownloadApp2.png\" alt=\"GitLab-Download-App GitLab同居 構成図\" width=\"826\" height=\"220\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>  \n\nこれについて、実現手順は、別記事で紹介しています。→「<a href=\"https://itc-engineering-blog.netlify.app/blogs/gitlab-nginx-php\" target=\"_blank\">GitLabバンドルnginxを利用してphpの独自Webアプリを同居させる手順</a>」\n\n<br />\n\n# 仕組み\n\n## GitLab API 利用\n\n画面からサーバーに指示を出し、サーバーからのみ GitLab API を利用しています。  \nこれにより、GitLab API トークンが画面に露出することは有りません。  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/GitLabDownloadApp3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/GitLabDownloadApp3.png\" alt=\"GitLab APIトークンが画面に露出することは有りません 図\" width=\"652\" height=\"231\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n画面から直接 GitLab API を利用する以下の作りの場合、画面を解析するとトークンが分かるため、トークンは、サーバー内に設定し、サーバー内でのみ読み込む作りとしました。  \n(まずい例)  \n↓  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/GitLabDownloadApp4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/GitLabDownloadApp4.png\" alt=\"画面から直接GitLab APIを利用する 図\" width=\"548\" height=\"412\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n## 一覧表示\n\nプロジェクト/commit/タグの一覧情報は、初回表示時に projects.json という全プロジェクト/commit/タグ情報の json を作成して、以降、それを使って表示しています。\nそのため、プロジェクト、commit が大量にあると、初回表示だけ時間がかかります。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/gitlab-download-app2.gif\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/gitlab-download-app2.gif\" alt=\"プロジェクト/commit/タグの一覧情報\" width=\"500\" height=\"267\" loading=\"lazy\"></a>\n\n<br />\n\nprojects.json の更新は、スクリプト単体でも行えるようにしました。cron で設定できます。\n\n```sh\n0 * * * * cd /opt/gitlab-download-app/www/html && /usr/bin/php refresh_projects_json.php\n```\n\n<br />\n\ncron に設定するスクリプトを以下のように事前に直接実行しておけば、初回表示に時間がかかることは有りません。\n\n```shellsession\n# su - www-data -s /bin/bash -c \"cd /opt/gitlab-download-app/www/html && /usr/bin/php refresh_projects_json.php\"\n```\n\n<br />\n\nタグ一覧画面も projects.json を読み込んで表示しています。  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/gitlab-download-app3.gif\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/gitlab-download-app3.gif\" alt=\"タグ一覧画面もprojects.jsonを読み込んで表示\" width=\"500\" height=\"151\" loading=\"lazy\"></a>\n\n<br />\n\n`projects.json`の自動更新がうまくいっていないと思われる場合は、「最新に更新」ボタンを押すと、`projects.json`が強制的に更新されます。\n\n<br />\n\n## WebHook\n\nprojects.json の更新は、WebHook を使って、commit があった時に追加されるようにもしています。  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/GitLabDownloadApp5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/GitLabDownloadApp5.png\" alt=\"WebHook 図\" style=\"margin-top: 5px;margin-bottom: 5px;\" width=\"816\" height=\"124\" loading=\"lazy\"></a>\n\nこれにより、リポジトリの状況をほぼリアルタイムで追随します。(万一壊れても、cron で仕掛けた`refresh_projects_json.php`により、刷新されます。)\n\n<br />\n\n## commit 履歴並び順\n\nGitLab API の  \n`[GitLab URL]/api/v4/projects/[プロジェクトid]/repository/commits`  \nから取得したコミット履歴順です。(新しい方が一番上)  \n`order`パラメータで`committed_date`などでソートされた結果を得られるのですが、何も指定しない場合、commits API のデフォルトになり、GitLab の画面と同等の結果が得られるようです。  \n結果、`git log --topo-order`の順序に従っているようです。  \n参考:<a href=\"https://git-scm.com/docs/git-log#_commit_ordering\" target=\"_blank\">git log Commit Ordering</a>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/GitLabDownloadApp6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/GitLabDownloadApp6.png\" alt=\"commit履歴並び順 図\" style=\"margin-top: 5px;margin-bottom: 5px;\" width=\"698\" height=\"670\" loading=\"lazy\"></a>\n\n<br />\n\n## ダウンロード\n\nGitLab API の\n`[GitLab URL]/api/v4/projects/[プロジェクトid]/repository/tree`  \nを再帰的に実行し、パスを把握し、  \n`[GitLab URL]/api/v4/projects/[プロジェクトid]/repository/files/[パス]/raw`  \nを使用して、実体ファイルを取得しています。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/gitlab-download-app1.gif\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/gitlab-download-app1.gif\" alt=\"「GitLab-Download-App」\" width=\"500\" height=\"190\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/GitLabDownloadApp7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/GitLabDownloadApp7.png\" alt=\"ダウンロード 図\" style=\"margin-top: 5px;margin-bottom: 5px;\" width=\"774\" height=\"669\" loading=\"lazy\"></a>\n\n<br />\n\nこのとき、  \n`[GitLab URL]/api/v4/projects/:id/repository/commits/[コミットID]/diff`  \nで過去までさかのぼって、削除されたことの把握と最終コミット日時を把握し、touch されてダウンロードされるようになっています。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/GitLabDownloadApp8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/GitLabDownloadApp8.png\" alt=\"削除されたことの把握と最終コミット日時を把握し、touchされてダウンロード 図\" style=\"margin-top: 5px;margin-bottom: 5px;\" width=\"342\" height=\"609\" loading=\"lazy\"></a>\n\n<br />\n\n## .tar.gz/.zip キャッシュ\n\n.tar.gz/.zip ダウンロードは、特に全体取得の場合、時間がかかります。そのため、同じプロジェクト、commit 日時の場合、GitLab API から取得せず、サーバーに置かれた.tar.gz/.zip を取得するように、キャッシュを持たせる仕組みとしました。\n\n<br />\n\n.tar.gz/.zip のファイル名は、gitlab.zip or gitlab.tar.gz 固定です。\n\n<br />\n\n「プロジェクト名を含んでダウンロード」にチェックが有る場合、.tar.gz/.zip 作成時に、[プロジェクト名]/ファイル群で圧縮されます。\n\n<br />\n\n**「プロジェクト名を含んでダウンロード」チェック無し例:**\n\n```shellsession\n$ unzip -l gitlab.zip\nArchive:  gitlab.zip\n  Length      Date    Time    Name\n---------  ---------- -----   ----\n        0  2021-12-13 21:55   app/\n        0  2021-12-13 21:55   app/views/\n        0  2021-12-13 21:55   app/views/welcome/\n       24  2020-12-19 01:28   app/views/welcome/index.html.erb\n```\n\n<br />\n\n**「プロジェクト名を含んでダウンロード」チェック有り例:**\n\n```shellsession\n$ unzip -l gitlab.zip\nArchive:  gitlab.zip\n  Length      Date    Time    Name\n---------  ---------- -----   ----\n        0  2021-12-13 21:55   TEST Project1/\n        0  2021-12-13 21:55   TEST Project1/app/\n        0  2021-12-13 21:55   TEST Project1/app/views/\n        0  2021-12-13 21:55   TEST Project1/app/views/welcome/\n       24  2020-12-19 01:28   TEST Project1/app/views/welcome/index.html.erb\n```\n\n<br />\n\n## ログ\n\nログは、`/tmp`に出力されます。  \n`refresh_projects_json.php`以外は、apache 経由で起動するため、systemctl の起動設定が`PrivateTmp = true`になっている場合、以下のようなパスに出力されます。  \nその場合、apache を再起動すると、消えます。\n\n**ログパス例:**\n\n```sh\n/tmp/systemd-private-717c61a6a51440c3912e8e8d0fbabc78-apache2.service-WMlg6f/tmp/update_projects_json.log\n/tmp/systemd-private-717c61a6a51440c3912e8e8d0fbabc78-apache2.service-WMlg6f/tmp/proxyproc.log\n/tmp/systemd-private-717c61a6a51440c3912e8e8d0fbabc78-apache2.service-WMlg6f/tmp/gitlab-download-app.log\n```\n\n<br />\n\n# インストール方法\n\nphp の Web アプリとして動作すれば良く、全くこの通りである必要はありません。  \nApache の場合のインストール方法を書きます。\n\n<blockquote class=\"warn\">\n<p>Ubuntu 20.04.2.0 LTSの場合です。CentOS等は手順が異なります。</p>\n<p>Apacheとphpのインストールについては、別記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/i-hna4_wx\">Ubuntu 20.04.2.0にapache2,php,postgresqlをインストール</a>」で丁寧めに説明していますので、細かい説明は省略して書きます。</p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>ここでは、全て以下のように表記します。</p>\n<p>GitLabサーバーFQDN:<code>gitlab.itccorporation.jp</code></p>\n<p>GitLab Download AppサーバーFQDN:<code>gitlab-download-app.itccorporation.jp</code></p>\n</blockquote>\n\n<br />\n\n## apacheインストール\n\ntimezone とロケールを調整しておきます。\n\n```shellsession\n# apt update\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\nhosts に GitLab-Download-App と GitLab のホスト名を登録します。\n\n```shellsession\n# vi /etc/hosts\n```\n\n```sh\n192.168.12.200 gitlab-download-app.itccorporation.jp\n192.168.12.111 gitlab.itccorporation.jp\n```\n\n<br />\n\n今回 GitLab-Download-App を`/opt/gitlab-download-app/www/html`にインストールするため、DocumentRoot を変更します。\n\n```shellsession\n# vi /etc/apache2/sites-available/000-default.conf\n```\n\n```sh\nServerName gitlab-download-app.itccorporation.jp\nDocumentRoot /var/www/html\n↓\nDocumentRoot /opt/gitlab-download-app/www/html\n\n# vi /etc/apache2/apache2.conf\n<Directory /var/www/>\n↓\n#<Directory /var/www/>\n<Directory /opt/gitlab-download-app/www/>\n```\n\n```shellsession\n# systemctl restart apache2\n```\n\n<br />\n\n## アクセストークン取得\n\n<span style=\"color: red;\">Administrator か、全プロジェクト見られる人で、</span>GitLab(ここでは、`http://gitlab.itccorporation.jp/`)にアクセスして、右上のアイコンクリック →Preference をクリックします。  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/image1.png\" alt=\"右上のアイコンクリック→Preferenceをクリック\" width=\"1244\" height=\"274\" loading=\"lazy\"></a>\n\n<br />\n\nアクセストークンをクリックします。  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/image2.png\" alt=\"アクセストークンをクリック\" width=\"1197\" height=\"408\" loading=\"lazy\"></a>\n\n<br />\n\n以下の権限のアクセストークンを取得します。  \n名前: `GitLab Download App`(任意)  \n有効期限日: `未入力`(無期限)  \nスコープ: `read_api`  \n「Create personal access token」をクリックします。  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/image3.png\" alt=\"「Create personal access token」をクリック\" width=\"1204\" height=\"936\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/image4.png\" alt=\"アクセストークン表示\" width=\"1198\" height=\"330\" loading=\"lazy\"></a>\n\n→ 以降、アクセストークン:`YRPfTcCL4Brx7YwyhJEo`として進めていきます。\n\n<blockquote class=\"warn\">\n<p>アクセストークンは、二度と表示されなくなりますので、画面を残したままにするか、メモ帳などにコピーペーストしておく必要があります。</p>\n</blockquote>\n\n<br />\n\n## GitLab Download Appセットアップ\n\napache の実行ユーザーを確認します。\n\n```shellsession\n# grep -E \"APACHE_RUN_(USER|GROUP)\" /etc/apache2/envvars\nexport APACHE_RUN_USER=www-data\nexport APACHE_RUN_GROUP=www-data\n```\n\n<br />\n\nGitLab Download App を`/opt/gitlab-download-app/www/html`に配置します。\n\n```shellsession\n# mkdir -p /opt/gitlab-download-app/www\n# git clone https://github.com/itc-lab/gitlab-download-app.git\n# mv gitlab-download-app /opt/gitlab-download-app/www/html\n# chown -R www-data:www-data /opt/gitlab-download-app/www/html\n```\n\n<br />\n\nGitLab Download App 用のキャッシュディレクトリを作成します。\n\n```shellsession\n# mkdir /opt/gitlab-download-app/cache\n# chown root:root /opt/gitlab-download-app/cache\n# chmod 1777 /opt/gitlab-download-app/cache\n```\n\n<br />\n\n設定を調整します。\n\n```shellsession\n# vi /opt/gitlab-download-app/www/html/config.json\n```\n\n```json\n{\n  \"url\": \"http://gitlab.itccorporation.jp/\",\n  \"ignore_commit_message\": [],\n  \"mark_commit_message\": [[\"Merge branch\", \"*\"]],\n  \"ignore_file_name\": [\"^\\\\.gitkeep\", \"^\\\\.gitignore\"],\n  \"cache_projects\": 1,\n  \"max_message_length\": 500,\n  \"group\": \"daemon\",\n  \"user\": \"daemon\"\n}\n```\n\nおのおの以下の意味です。\n\n```json\n{\n  \"url\": GitLabのURL,\n  \"ignore_commit_message\": commitメッセージにこの単語が含まれていた場合、一覧に表示しない。,\n  \"mark_commit_message\": commitメッセージにこの単語が含まれている場合、commit日時の前にマークを表示(単語,マークの順セットで記述),\n  \"ignore_file_name\": このファイルをダウンロード対象に含めない。,\n  \"cache_projects\": 1以外の場合、キャッシュを無効にする。,\n  \"max_message_length\": commitメッセージの表示を500文字までとする。,\n  \"group\": .tar.gzのgroup,\n  \"user\": .tar.gzのuser\n}\n```\n\n<br />\n\nアクセストークンを設定します。\n\n```shellsession\n# vi /opt/gitlab-download-app/www/html/function.inc\n```\n\n```php\ndefine(\"ACCESS_TOKEN\", \"YRPfTcCL4Brx7YwyhJEo\");\n```\n\n<br />\n\nプロジェクト/commit 一覧のキャッシュファイル`/opt/gitlab-download-app/cache/projects.json`を作成しておきます。\n\n```shellsession\n# su - www-data -s /bin/bash -c \"cd /opt/gitlab-download-app/www/html && /usr/bin/php refresh_projects_json.php\"\n```\n\n※初回アクセスで作成しても良いです。\n\n<br />\n\nプロジェクト/commit 一覧のキャッシュファイル更新処理を crontab に登録します。\n\n```shellsession\n# crontab -u www-data -e\n```\n\n```sh\n0 * * * * cd /opt/gitlab-download-app/www/html && /usr/bin/php refresh_projects_json.php\n```\n\n<br />\n\n## System Hooks 設定\n\nプロジェクトごとに WebHook を設定できますが、全体に効くように System Hooks を使います。\n\n<blockquote class=\"info\">\n<p>【 WebHook 】</p>\n<p>「WebHook」とは、データ更新などのイベントが発生したら、外部のアプリやサービスに通知を送り、連携する仕組みのことです。「WebHook」は、GitLabに限らない用語で、「System Hooks」の方は、GitLab用語でシステム全体のイベントを捉えるWebHookのことのようです。</p>\n</blockquote>\n\n管理者エリア → システムフック にて  \nURL: `http://gitlab-download-app.itccorporation.jp/update_projects_json.php`  \nSecret token: `空白`  \nトリガー: `プッシュイベント`と`Tag push events`  \nSSL verification: `Enable SSL verificationチェック無し`  \nとし、「システムフックの追加」ボタンをクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/image5.png\" alt=\"管理者エリア→システムフック\" width=\"1214\" height=\"642\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/image6.png\" alt=\"「システムフックの追加」ボタンをクリック\" width=\"1214\" height=\"736\" loading=\"lazy\"></a>\n\n<br />\n\nこれにより、`projectcs.json`がプロジェクト作成、commit、push に追随し、常に、GitLab の状態= GitLab Download App の状態になります。  \n万が一追随しきれなかった場合、crontab の`refresh_projects_json.php`にて GitLab の状態= GitLab Download App の状態になります。\n\n<br />\n\n# 利用例\n\n・コミット選択、コミット履歴表示  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/gitlab-download-app4.gif\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/gitlab-download-app4.gif\" alt=\"コミット選択、コミット履歴表示\" width=\"500\" height=\"168\" loading=\"lazy\"></a>\n\n<br />\n\n・全体ダウンロード\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/gitlab-download-app1.gif\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/gitlab-download-app1.gif\" alt=\"全体ダウンロード\" width=\"500\" height=\"190\" loading=\"lazy\"></a>\n\n<br />\n\n・差分ダウンロード  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/gitlab-download-app5.gif\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/gitlab-download-app5.gif\" alt=\"差分ダウンロード\" width=\"500\" height=\"191\" loading=\"lazy\"></a>\n\n<br />\n\n・タグ選択  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/gitlab-download-app3.gif\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/gitlab-download-app3.gif\" alt=\"タグ選択\" width=\"500\" height=\"151\" loading=\"lazy\"></a>\n\n<br />\n\n・プロジェクト名を含んでダウンロード  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/gitlab-download-app6.gif\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/gitlab-download-app6.gif\" alt=\"プロジェクト名を含んでダウンロード\" width=\"500\" height=\"231\" loading=\"lazy\"></a>\n\n<br />\n\n・グループ絞り込み  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/gitlab-download-app7.gif\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/gitlab-download-app7.gif\" alt=\"グループ絞り込み\" width=\"500\" height=\"229\" loading=\"lazy\"></a>\n\n<br />\n\n・ソート  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/gitlab-download-app8.gif\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/gitlab-download-app8.gif\" alt=\"ソート\" width=\"500\" height=\"231\" loading=\"lazy\"></a>\n\n<br />\n\n# おまけ\n\nWebHook のところで、システムフックを登録する手順がありますが、最初、プロジェクト全てに WebHook の設定をしていかないといけないと思いました。  \n結局、システムフック1個登録するだけでしたが、プロジェクト全てに WebHook の設定ができるツールを作成しましたので、gist.githubに公開しました。  \nPHP 版:<a href=\"https://gist.github.com/itc-lab/dd3635dabae2bbfc633c9cbf1323d251\" target=\"_blank\">`https://gist.github.com/itc-lab/dd3635dabae2bbfc633c9cbf1323d251`</a>  \nPython 版:<a href=\"https://gist.github.com/itc-lab/357ad01f5fbc5ea6929109784198913d\" target=\"_blank\">`https://gist.github.com/itc-lab/357ad01f5fbc5ea6929109784198913d`</a>\n\n<br />\n\nPHP 版:\n\n```\n# php gitlab-add-hook.php testgroup1/test-project1\n```\n\nPython 版:\n\n```\n# python3 gitlab-add-hook.py testgroup1/test-project1\n```\n\nとすると、プロジェクト`testgroup1/test-project1`に WebHook が登録されます。登録される WebHook の内容は、ソースコードに直書きです。  \n引数は、プロジェクトの name space です。  \n引数無しの場合、全プロジェクトに同じ WebHook が登録されます。\n\n<blockquote class=\"warn\">\n<p>トークンのスコープは、\"api\"が必要です。\"read_api\"では登録できません。</p>\n</blockquote>\n","description":"GitLab API を利用した一括ダウンロードWebアプリを php で作りました。GitHub に公開しています。作成した「GitLab-Download-App」の紹介記事です。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"syq-6kny4","createdAt":"2021-12-15T12:20:20.608Z","updatedAt":"2021-12-15T12:20:20.608Z","publishedAt":"2021-12-15T12:20:20.608Z","revisedAt":"2021-12-15T12:20:20.608Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-download-app/ITC_Engineering_Blog.png","alt":"GitLab APIを利用した一括ダウンロードWebアプリを作った","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>有効になるまでに約2日</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今回、認証については関係無いので、デプロイに成功し、画面が表示されたらOKとします。\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` のような内容に書き換えればOKです。  \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":"kotless-sendgrid","createdAt":"2022-05-30T13:55:48.934Z","updatedAt":"2022-05-30T13:55:48.934Z","publishedAt":"2022-05-30T13:55:48.934Z","revisedAt":"2022-05-30T13:55:48.934Z","title":"LocalStack Kotless(Kotlin) SendGridのオフライン動作環境構築","category":{"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},"topics":[{"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":"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":"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\nLocalStack、Kotlin のサーバーレスフレームワーク Kotless、メール配信サービスの SendGrid のモック環境を構築しました。  \nメール配信サービスの SendGrid は、本来クラウドのサービス(SMTP or Web API)ですが、モックを使ってオフラインで動作確認する方法を見つけて使ってみました。\n\n<br />\n\n見つけた SendGrid のモックは、以下です。  \n参考記事:<a href=\"https://engineer.blog.lancers.jp/docker/sendgrid-maildev/\" target=\"_blank\">SendGrid 用の Mail モックコンテナを作りました(https://engineer.blog.lancers.jp/docker/sendgrid-maildev/)</a>  \nソースコード:<a href=\"https://github.com/yKanazawa/sendgrid-dev\" target=\"_blank\">https://github.com/yKanazawa/sendgrid-dev</a>\n\n<br />\n\n上記参考記事にもありますが、SMTP サーバーも MailDev を利用して、モックで構築しました。  \n結果、メール送信プログラムのデプロイ先(AWS のモック)、メール配信 API(SendGrid のモック)、SMTP サーバー、全てモックでメール送信プログラムの動作確認ができるようになりました。  \n全てモックのため、オフラインで動作確認可能です。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-sendgrid/kotless-sendgrid1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-sendgrid/kotless-sendgrid1.png\" alt=\"LocalStack、Kotless、MailDev、SendGridのモック環境 図\" width=\"982\" height=\"843\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n↑  \nAWS - SendGrid - SMTP サーバー - メールボックス の疑似環境全てをオフラインに閉じ込めてテストできます。  \nこれにより、メール送信プログラムの実装をミスっても問題無くなります。\n\n<br />\n\n<blockquote class=\"alert\">\n<p>ネットワーク設定などにより、事故が起きる可能性はあります。</p>\n<p><span style=\"color: red;\"><strong>本記事情報の誤り、見落とし、考慮不足等により何らかの問題が生じても、一切責任を負いません。</strong></span></p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>デプロイ先、向き先を</p>\n<p>LocalStack → 本物のAWS</p>\n<p>sendgrid-dev → 本物のSendGrid</p>\n<p>と置き換えれば、動作するはずですが、本物に置き換えての動作確認はしていません。</p>\n</blockquote>\n\n<br />\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<p> Ubuntu 20.04.2 LTS</p>\n<p>  node 14.19.3</p>\n<p>  npm 6.14.17</p>\n<p>  maildev 2.0.5</p>\n<p>  sendgrid-dev 0.9.1</p>\n<p>  go 1.18.2</p>\n</blockquote>\n\n<br />\n\n# MailDev インストール\n\nDocker を使わず、ビルドしていきましたので、ビルドの手順になります。\n\n<br />\n\n## node,npm インストール\n\napt でインストールした `nodejs v10.19.0` の場合、`TextEncoder is not defined` となり、maildev の起動に失敗したため、`v14.x` をインストールしました。\n\n```shellsession:v10のmaildev起動時エラー\n# maildev\n/usr/local/lib/node_modules/maildev/node_modules/whatwg-url/lib/encoding.js:2\nconst utf8Encoder = new TextEncoder();\n                    ^\n\nReferenceError: TextEncoder is not defined\n```\n\n<br />\n\n```shellsession:v14.xインストール手順\n# curl -sL https://deb.nodesource.com/setup_14.x -o nodesource_setup.sh\n# bash nodesource_setup.sh\n# apt update\n# apt -y install nodejs\n# node -v\nv14.19.3\n\n# npm -v\n6.14.17\n```\n\n<br />\n\n## maildev インストール\n\nmaildev は、コマンドとして使いたいので、グローバルに `npm install` します。\n\n```shellsession\n# npm install -g maildev\n# maildev -V\n2.0.5\n```\n\n<br />\n\n<blockquote class=\"info\">\n<p>特に指定が無い場合、メールデータは、<code>/tmp/maildev-3621</code> のように <code>/tmp/</code> 配下になります。<code>maildev-</code> の後の数字部分は毎回ランダムに決まるようです。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>SMTP は 1025、Webは 1080 ポートで待ち受けます。それぞれ、<code>-s</code> <code>-w</code> オプションで変更可能です。</p>\n</blockquote>\n\n<br />\n\n## MailDev 動作確認\n\nSMTP サーバー、Web 画面の動作確認を行います。\n\nMailDev を起動します。\n\n```shellsession\n# mkdir /home/admin/mail\n# maildev --mail-directory /home/admin/mail\n```\n\n<blockquote class=\"info\">\n<p>なんとなく、メールデータを消えないところに置きたかったので、<code>--mail-directory</code> で指定して起動しています。</p>\n</blockquote>\n\n<br />\n\n動作確認用に telnet、nkf をインストールします。\n\n```shellsession\n# apt install -y telnet\n# apt install -y nkf\n```\n\nnkf は、日本語のメールを送信したいため、インストールしました。  \nコピペ用に MIME エンコードの値を知りたいためにインストールしただけで、必須ではありません。\n\n<br />\n\n以下のように base64 の値をあらかじめ調べておきます。  \nメール件名:ISO-2022-JP の base64 エンコードヘッダ形式  \nメール本文:ISO-2022-JP の base64 エンコード  \nです。\n\n```shellsession\n# echo 日本語件名 | nkf -jM\n=?ISO-2022-JP?B?GyRCRnxLXDhsN29MPhsoQg==?=\n\n# echo 日本語本文 | nkf -jMB\nGyRCRnxLXDhsS1xKOBsoQgo=\n```\n\n調べた値を使って、以下のように  \n件名:日本語件名  \n本文:日本語本文  \nのメールを telnet で送信します。\n\n```shellsession\n# telnet localhost 1025\nHELO maildev.example.com\nMAIL FROM: <from@maildev.example.com>\nRCPT TO: <rcpt@maildev.example.com>\ndata\nFrom: \"sender\" <sender@maildev.example.com>\nTo: \"rcpt\" <rcpt@maildev.example.com>\nSubject: =?ISO-2022-JP?B?GyRCRnxLXDhsN29MPhsoQg==?=\nMIME-Version: 1.0\nContent-Type: text/plain; charset=\"iso-2022-jp\"\nContent-Transfer-Encoding: base64\n\nGyRCRnxLXDhsS1xKOBsoQgo=\n.\nquit\n```\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-sendgrid/kotless-sendgrid2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-sendgrid/kotless-sendgrid2.png\" alt=\"MailDev動作確認 telnet localhost 1025 図\" width=\"982\" height=\"843\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\nWeb 画面(`http://localhost:1080/`)へアクセスしてみます。\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-sendgrid/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-sendgrid/image1.png\" alt=\"MailDev動作確認 文字化け\" width=\"1101\" height=\"270\" loading=\"lazy\"></a>\n\n<br />\n\n<span style=\"color: red;\"><strong>日本語が文字化けして表示されました。</strong></span>\n\n<br />\n\n<span style=\"color: red;\"><strong>iconv 組み込みによって、これを解消します。</strong></span>\n\n<br />\n\n## iconv 組み込み\n\n日本語文字化け解消のため、maildev に iconv を追加します。\n\n```\n# apt -y install build-essential\n# cd /usr/lib/node_modules/maildev\n# npm install iconv\n```\n\n<blockquote class=\"info\">\n<p><code># apt -y install build-essential</code> は、<code>gyp ERR! stack Error: not found: make</code> とエラーになったため、<code>make</code> を事前にインストールしています。</p>\n</blockquote>\n\n<br />\n\n## maildev 再動作確認\n\nCTRL+C で止めて再度起動します。\n\n```shellsession\n# maildev --mail-directory /home/admin/mail\n```\n\nWeb 画面(`http://localhost:1080/`)へアクセスしてみます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-sendgrid/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-sendgrid/image2.png\" alt=\"MailDev動作確認 文字化け解消\" width=\"1103\" height=\"295\" loading=\"lazy\"></a>\n\n<br />\n\n文字化けが解消しました!\n\n<br />\n\n# sendgrid-dev インストール\n\nSendGrid Web API のモック sendgrid-dev をインストールします。\n\n## go インストール\n\nsendgrid-dev は、Go 言語(golang)製のため、go をダウンロードして、インストールします。\n\n```shellsession\n# wget https://go.dev/dl/go1.18.2.linux-amd64.tar.gz\n# tar zxf go1.18.2.linux-amd64.tar.gz -C /usr/local/\n# vi ~/.profile\n```\n\n以下を追記します。\n\n```sh\nexport PATH=$PATH:/usr/local/go/bin\n```\n\n```shellsession\n# source ~/.profile\n# go version\ngo version go1.18.2 linux/amd64\n```\n\n<blockquote class=\"info\">\n<p>直接置いて、パスを通すだけです。最新バージョンは、go公式ページ(<a href=\"https://go.dev/dl/\" target=\"_blank\">https://go.dev/dl/</a>)で確認できます。</p>\n</blockquote>\n\n<br />\n\n## sendgrid-dev ビルド\n\n<a href=\"https://github.com/yKanazawa/sendgrid-dev\" target=\"_blank\">`https://github.com/yKanazawa/sendgrid-dev`</code> からソースコードの zip をダウンロードして、`go run` でビルド&起動します。\n\n```shellsession\n# unzip sendgrid-dev-master.zip\n# cd sendgrid-dev-master\n# go run main.go\nSENDGRID_DEV_API_SERVER :3030\nSENDGRID_DEV_API_KEY SG.xxxxx\nSENDGRID_DEV_SMTP_SERVER 127.0.0.1:1025\nSENDGRID_DEV_SMTP_USERNAME\nSENDGRID_DEV_SMTP_PASSWORD\n\n   ____    __\n  / __/___/ /  ___\n / _// __/ _ \\/ _ \\\n/___/\\__/_//_/\\___/ v3.3.10-dev\nHigh performance, minimalist Go web framework\nhttps://echo.labstack.com\n____________________________________O/_______\n                                    O\\\n? http server started on [::]:3030\n```\n\nポート:`:3030`  \nAPI キー:`SG.xxxxx`  \nSMTP サーバー:`127.0.0.1:1025`  \nで起動します。\n\n<br />\n\nSMTP サーバーは、MailDev がちょうど、`localhost:1025`ポートですので、何も変更する必要は有りませんでした。(デフォルトで、MailDev との連携が想定されているからです。)  \n変更する場合、環境変数をセットしてから起動します。\n\n<br />\n\n## sendgrid-dev 動作確認\n\n(maildev が起動しているものとします。)\n\n```shellsession\n# apt -y install curl\n# curl --request POST \\\n  --url http://localhost:3030/v3/mail/send \\\n  --header 'Authorization: Bearer SG.xxxxx' \\\n  --header 'Content-Type: application/json' \\\n  --data '{\"personalizations\": [{\n    \"to\": [{\"email\": \"to@example.com\"}]}],\n    \"from\": {\"email\": \"from@example.com\"},\n    \"subject\": \"Test Subject\",\n    \"content\": [{\"type\": \"text/plain\", \"value\": \"Test Content\"}]\n  }'\n```\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-sendgrid/kotless-sendgrid3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-sendgrid/kotless-sendgrid3.png\" alt=\"sendgrid-dev 動作確認 curl 図\" width=\"981\" height=\"915\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\nWeb 画面(`http://localhost:1080/`)へアクセスしてみます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-sendgrid/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-sendgrid/image3.png\" alt=\"sendgrid-dev 動作確認 curlのメール受信\" width=\"1100\" height=\"291\" loading=\"lazy\"></a>\n\n<br />\n\nメールを受信しています!\n\n<br />\n\n# Kotless デプロイ\n\nデプロイ先:LocalStack(AWS Lambda のローカル環境版)  \nデプロイするもの:Kotlin のサーバーレスフレームワーク Kotless プログラム  \nを準備します。\n\n<br />\n\nLocalStack & Kotless の詳しいことは、当ブログ別記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/kotless-localstack-s3\" target=\"_blank\">Kotless と LocalStack で疑似サーバーレス - AWS S3 ダウンロード</a>」に書きましたので、大部分端折って、プログラム作成部分だけ書きます。\nLocalStack の準備、JDK、IntelliJ インストールの部分や、設定の意味は、上記記事を見てください。\n\n<br />\n\n`/home/share/sendgrid`  \nに Kotlin のプロジェクトを新規作成します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-sendgrid/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-sendgrid/image4.png\" alt=\"IntelliJ IDEA Kotless プロジェクト新規作成\" width=\"802\" height=\"635\" loading=\"lazy\"></a>\n\n<br />\n\n`settings.gradle.kts`、`gradle.properties`、`build.gradle.kts` をそれぞれ以下のようにします。\n\n```kts:settings.gradle.kts\nrootProject.name = \"sendgrid\"\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```properties:gradle.properties\nkotlin.code.style=official\nawsSdkKotlinVersion=0.+\n```\n\n<br />\n\n```kts: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.sendgrid\"\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(\"com.sendgrid\", \"sendgrid-java\", \"4.9.2\")\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.sendgrid.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<br />\n\nメール送信プログラム `src/main/kotlin/org/example/sendgrid/Main.kt` を作成します。\n\n```kotlin:Main.kt\npackage org.example.sendgrid\n\nimport com.sendgrid.Method\nimport com.sendgrid.Request\nimport com.sendgrid.SendGrid\nimport com.sendgrid.helpers.mail.Mail\nimport com.sendgrid.helpers.mail.objects.Content\nimport com.sendgrid.helpers.mail.objects.Email\nimport io.kotless.dsl.lang.http.Get\nimport org.slf4j.LoggerFactory\nimport java.io.IOException\n\nprivate val logger = LoggerFactory.getLogger(\"sendgrid\")\n\n@Get(\"/\")\nfun main() = \"Hello world!\"\n\n@Get(\"/sendmail\")\nfun sendmail(mailTo: String = \"\"): String = sendBySendGrid(mailTo)\n\nfun sendBySendGrid(mailTo: String): String  {\n    logger.info(\"sendBySendGrid start\")\n    val from = Email(\"test@example.com\")\n    val subject = \"Sending with SendGrid is Fun/SendGridで送信するのは楽しいです\"\n    val to = Email(mailTo)\n    val content = Content(\"text/plain\", \"and easy to do anywhere, even with Kotlin\\nKotlinを使用しても、どこでも簡単に実行できます\")\n    val mail = Mail(from, subject, to, content)\n\n    val sg = SendGrid(\"SG.xxxxx\", true)\n    sg.host = \"192.168.12.206:3030\"\n    val request = Request()\n    logger.info(\"sendBySendGrid request\")\n    try {\n        request.method = Method.POST\n        request.endpoint = \"mail/send\"\n        request.body = mail.build()\n        val response = sg.api(request)\n        logger.info(\"sendBySendGrid response:${response.statusCode}\")\n        return(response.statusCode.toString())\n    } catch (ex: IOException) {\n        logger.info(\"sendBySendGrid Error\")\n        throw ex\n    }\n}\n```\n\nプログラムは、  \n<a href=\"https://sendgrid.kke.co.jp/blog/?p=8471\" target=\"_blank\">Kotlin から SendGrid を利用してメール送信する(https://sendgrid.kke.co.jp/blog/?p=8471)</a>  \nを参考にしました。  \nというか、ほぼ、そのままですが、  \n<span style=\"color: red;\"><strong>今回オフラインで動作させたいため、以下の部分が重要になります。</strong></span>\n\n<br />\n\n●<span style=\"color: red;\"><strong>`val sg = SendGrid(\"SG.xxxxx\", true)`</strong></span>  \nSendGrid の最初の引数は、API キーですが、2番目の引数は、今回のようなテスト環境の場合、`true` にする必要があります。\n<span style=\"color: red;\"><strong>これにより、`https://` ではなく、`http://` でアクセスするようになります。</strong></span>  \nsendgrid-dev は、`http://` のため、`true` にする必要があります。\n\n<br />\n\n●<span style=\"color: red;\"><strong>`sg.host = \"192.168.12.206:3030\"`</strong></span>  \n<span style=\"color: red;\"><strong>sendgrid-dev のホスト:ポートにしないと、本物の SendGrid へ行ってしまいます。</strong></span>  \n本物の SendGrid へ行ってしまったときは、`401` が返ります。\n\n<br />\n\nLocalStack に Kotless プログラムをデプロイして、リクエストを出してみます。\n\n```shellsession\n# cd /home/share/sendgrid\n# chmod 755 gradlew\n# ./gradlew local\n```\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-sendgrid/kotless-sendgrid4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-sendgrid/kotless-sendgrid4.png\" alt=\"LocalStackにKotlessプログラムをデプロイ 図\" width=\"981\" height=\"843\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n(別端末で)\n\n```shellsession\n$ curl http://localhost:8080\nHello world!\n```\n\n`@Get(\"/\")`  \n`fun main() = \"Hello world!\"`  \nが動作して、  \n正常にデプロイされていることが確認できました!\n\n<br />\n\n# メール送信動作確認\n\nMailDev(SMTP サーバー)  \nsendgrid-dev(SendGrid モック API)  \nは起動しているものとします。\n\n<br />\n\n`http://localhost:8080/sendmail`  \nを起動します。\n\n```shellsession\n$ curl http://localhost:8080/sendmail?mailTo=hogehoge@example.com\n202\n```\n\n202 が返れば正常です。\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-sendgrid/kotless-sendgrid5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-sendgrid/kotless-sendgrid5.png\" alt=\"Kotless メール送信動作確認 図\" width=\"981\" height=\"843\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\nWeb 画面(`http://localhost:1080/`)へアクセスしてみます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-sendgrid/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-sendgrid/image5.png\" alt=\"Kotless メール送信動作確認\" width=\"1103\" height=\"390\" loading=\"lazy\"></a>\n\n<br />\n\nメールを受信しています!\n\n<br />\n\nヨシ!!\n\n<br />\n\n# エラーまとめ\n\n<br /><hr style=\"height:1px;border-width:0;color:gray;background-color:gray\"><br />\n\n**エラー内容:**\n\n```shellsession\n# maildev\n```\n\n<span style=\"background-color: cornsilk; color: red;\">`/usr/local/lib/node_modules/maildev/node_modules/whatwg-url/lib/encoding.js:2`</span>  \n<span style=\"background-color: cornsilk; color: red;\">`const utf8Encoder = new TextEncoder();`</span>  \n<span style=\"background-color: cornsilk; color: red;\">`ReferenceError: TextEncoder is not defined`</span>\n\n<br />\n\n**原因:**  \nnode のバージョンが低い。\n\n<br />\n\n**対処内容:**\n\nNode v14 にする。\n\n```shellsession\n# curl -sL https://deb.nodesource.com/setup_14.x -o nodesource_setup.sh\n# bash nodesource_setup.sh\n# apt -y install nodejs\n# node -v\nv14.19.3\n```\n\n<br /><hr style=\"height:1px;border-width:0;color:gray;background-color:gray\"><br />\n\n**エラー内容:**\n\n```shellsession\n# cd /usr/lib/node_modules/maildev\n# npm install iconv\n```\n\n<span style=\"background-color: cornsilk; color: red;\">`gyp ERR! build error`</span>  \n<span style=\"background-color: cornsilk; color: red;\">`gyp ERR! stack Error: not found: make`</span>  \n<span style=\"background-color: cornsilk; color: red;\">`gyp ERR! stack at getNotFoundError (/usr/lib/node_modules/npm/node_modules/which/which.js:13:12)`</span>\n\n<br />\n\n**原因:**  \n`make` がインストールされていない。\n\n<br />\n\n**対処内容:**\n\n```shellsession\n# apt -y install build-essential\n```\n\n<br /><hr style=\"height:1px;border-width:0;color:gray;background-color:gray\"><br />\n\n**エラー内容:**\n\n```shellsession\n$ curl http://localhost:8080/sendmail?mailTo=hogehoge@example.com\n```\n\n<span style=\"background-color: cornsilk; color: red;\">`May 29, 2022 7:33:05 PM org.apache.http.impl.execchain.RetryExec execute`</span>  \n<span style=\"background-color: cornsilk; color: red;\">`INFO: I/O exception (java.net.NoRouteToHostException) caught when processing request to {s}->https://192.168.12.202:3030: No route to host`</span>\n<span style=\"background-color: cornsilk; color: red;\">`19:33:05.209 [qtp233996206-16] ERROR i.k.dsl.app.http.RoutesDispatcher - Failed on call of function sendmail`</span>\n\n<br />\n\n**原因:**  \nsendgrid-dev へ `https://` でアクセスしている。\n\n<br />\n\n**対処内容:**  \n`http://` でアクセスするように修正。(二番目の引数に`true`をセットすることにより、テストモードにする。)\n\n```kotlin\n    val sg = SendGrid(\"SG.xxxxx\", true)\n    sg.host = \"192.168.12.206:3030\"\n```\n\n<br /><hr style=\"height:1px;border-width:0;color:gray;background-color:gray\"><br />\n","description":"LocalStack、Kotlin のサーバーレスフレームワーク Kotless、メール配信サービスの SendGrid のモック環境を構築しました。構成の説明と構築手順の記事になります。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"akbsg7v5kwtc","createdAt":"2022-05-30T13:45:57.688Z","updatedAt":"2022-05-30T13:45:57.688Z","publishedAt":"2022-05-30T13:45:57.688Z","revisedAt":"2022-05-30T13:45:57.688Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/kotless-sendgrid/ITC_Engineering_Blog.png","alt":"LocalStack Kotless(Kotlin) SendGridのオフライン動作環境構築","width":1200,"height":630}],"seo_authors":[]},{"id":"apereo-cas-azuread","createdAt":"2024-02-17T12:04:24.951Z","updatedAt":"2024-03-02T10:18:29.854Z","publishedAt":"2024-02-17T12:04:24.951Z","revisedAt":"2024-03-02T10:18:29.854Z","title":"Apereo CASでDelegated Authentication with SAML2によるAzure AD SSO環境構築","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":"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":"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":"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\nApereo CAS Server で Delegated Authentication with SAML2 による Azure AD(Azure Active Directory/Microsoft Entra ID) SSO 環境を構築しました。  \n...紐解くと以下の話になります。  \n普通の SAML 認証と CAS を介して Azure AD 認証の2段階実施したということです。\n\n<br />\n\n**1.** SAML 認証対応アプリケーション(PHP)&Shibboleth(SAML SP) 作成  \n**2.** CAS Server(SAML IdP) 作成  \n**3.** 1と2で SAML 認証実現  \n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image1.png\" alt=\"普通の SAML 認証 Shibboleth(SAML SP) 図\" width=\"801\" height=\"391\" loading=\"lazy\"></a>\n\n<br />\n\n**4.** CAS Server の Delegated Authentication を設定  \n**5.** Shibboleth → CAS Server → Azure AD の SAML 認証実現  \n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image2.png\" alt=\"CAS を介して Azure AD 認証 図\" width=\"1031\" height=\"471\" loading=\"lazy\"></a>\n\n<br />\n\n今回、上記の流れのすべての手順を紹介していきます。\n\n<blockquote class=\"info\">\n<p>【 CAS 】</p>\n<p>CAS(Central Authentication Service)サーバーは、複数の Web アプリケーションにシングルサインオン(SSO)ソリューションを提供する集中認証サーバーです。</p>\n<p>主に Java で実装されているオープンソースです。(<code>https://github.com/apereo/cas</code>)</p>\n<p>OpenID Connect にも対応しますが、今回は、SAML で利用します。</p>\n</blockquote>\n\n<blockquote class=\"alert\">\n<p>(2024 年 2 月時点)</p>\n<p>RC(Release Candidate)以外の最新は、v7.0.1 ですが、まだ新しすぎるため、今回の記事では、6.6.x 系最新の Apereo CAS Server 6.6.15 で実施しています。</p>\n<p>また、今回、<span style=\"color: red;\"><strong>Docker で立ち上げません。ビルドして立ち上げます。</strong></span></p>\n</blockquote>\n\n<blockquote class=\"alert\">\n<p>何も設定しない場合の CAS Server URL は、</p>\n<p><code>https://cas.example.org:8443/cas/</code></p>\n<p>ですが、</p>\n<p><code>https://cas.example.com:8443/cas/</code></p>\n<p>でいきます。(.org → .com)</p>\n<p>※こういうことをして、どハマりするのは、あるあるなのですが、あえて踏み込みます。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 Shibboleth 】</p>\n<p>Shibboleth は、組織内および組織を超えて Web 上でフェデレーション・シングルサイオン(SSO)を実現する、標準的なオープンソースソフトウェアのパッケージです。</p>\n<p>サービスプロバイダ(Service Provider/SP)、アイデンティティプロバイダ(Identitiy Provider/IdP)両方の機能を持ちますが、今回は、SP として利用します。</p>\n<p>また、OpenID Connect にも対応しますが、今回は、SAML で利用します。</p>\n</blockquote>\n\n<blockquote class=\"info\">\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>●CAS Server</p>\n<p>・Ubuntu 22.04.3 LTS</p>\n<p> ・Apereo CAS Server 6.6.15</p>\n<p>●Apache & Shibboleth & PHP</p>\n<p>・Ubuntu 22.04.3 LTS(上記環境に同居)</p>\n<p> ・Apache 2.4.52</p>\n<p> ・shibboleth 3.3.0</p>\n<p> ・PHP 8.1.2</p>\n<p>●IntelliJ IDEA Community 2023.3.3</p>\n<p>● クラウド:Azure AD(Azure Active Directory/Microsoft Entra ID)</p>\n</blockquote>\n\n<br />\n\n# CAS Server ビルド\n\n<blockquote class=\"info\">\n<p>基本的には、公式ドキュメント(<code>https://apereo.github.io/cas/6.6.x/developer/Build-Process.html</code>)を参考にして実行しています。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>root 権限で作業しています。</p>\n</blockquote>\n\nCAS Server 6.6.15 を `git clone` で入手します。\n\n```shellsession\n# apt update -y\n# apt install git -y\n# cd /opt\n# git clone --depth=1 --single-branch --branch=v6.6.15 https://github.com/apereo/cas.git cas-server\n```\n\n<br />\n\nOpenJDK 11 をインストールします。\n\n```shellsession\n# apt install openjdk-11-jdk -y\n```\n\n<blockquote class=\"info\">\n<p>11 である理由は、公式ドキュメント(<code>https://apereo.github.io/cas/6.6.x/planning/Installation-Requirements.html</code>)に書いてあるからです。</p>\n</blockquote>\n\n<br />\n\nプロジェクトトップで、`gradlew` を使ってビルドします。\n\n<blockquote class=\"warn\">\n<p><span style=\"color: red;\">注意:環境により異なりますが、時間がかかります。30分以上かかるかもしれません。</span></p>\n</blockquote>\n\n```shellsession\n# cd /opt/cas-server\n# ./gradlew build --parallel -x test -x javadoc -x check --build-cache --configure-on-demand\n````\n\n<blockquote class=\"info\">\n<p><code>./gradlew build</code>:Gradle ラッパーを使用して <code>build</code> タスクを実行します。これにより、プロジェクトがコンパイルされ、テストが実行され、JAR が生成されます。</p>\n<p><code>--parallel</code>:可能な場合、プロジェクトのサブプロジェクトを並行して実行します。これにより、ビルド時間が短縮されます。</p>\n<p><code>-x test</code>:<code>test</code> タスクを除外します。つまり、テストは実行されません。</p>\n<p><code>-x javadoc</code>:<code>javadoc</code> タスクを除外します。つまり、JavaDoc は生成されません。</p>\n<p><code>-x check</code>:<code>check</code> タスクを除外します。これにより、静的コード解析などのチェックは実行されません。</p>\n<p><code>--build-cache</code>:ビルドキャッシュを有効にします。これにより、以前のビルドから再利用可能なタスク出力がキャッシュされ、ビルド時間が短縮されます。</p>\n<p><code>--configure-on-demand</code>:設定をオンデマンドにします。これにより、現在実行中のタスクに関連しないプロジェクトの設定フェーズがスキップされ、ビルド時間が短縮されます。</p>\n</blockquote>\n\n<br />\n\n# Web アプリビルド\n\nCAS Server サブプロジェクトのアプリケーションサーバーをビルドします。  \nいきなりビルドしてもうまくいかないため、その前に準備します。\n\n<br />\n\n`https://` で動作するため、公開鍵/秘密鍵のペアを生成し、キーストアに保存します。\n\n```shellsession\n# mkdir /etc/cas\n# keytool -genkey -alias cas -keyalg RSA -validity 999 \\\n    -keystore /etc/cas/thekeystore -ext san=dns:cas.example.com\n```\n\n<blockquote class=\"warn\">\n<p>このとき、パスフレーズの入力を求められます。</p>\n<p>今回は、<code>xxxxxx</code> とします。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p><code>keytool -genkey</code>:新しい公開鍵/秘密鍵のペアを生成します。</p>\n<p><code>-alias cas</code>:生成されたキーのエイリアスを <code>cas</code> とします。エイリアスは、キーストア内のキーを一意に識別するための名前です。</p>\n<p><code>-keyalg RSA</code>:キーのアルゴリズムを RSA とします。RSA は公開鍵暗号の一種で、広く使用されています。</p>\n<p><code>-validity 999</code>:生成されたキーの有効期間を <code>999</code> 日とします。</p>\n<p><code>-keystore /etc/cas/thekeystore</code>:生成されたキーを <code>/etc/cas/thekeystore</code> というパスのキーストアに保存します。</p>\n<p><code>-ext san=dns:cas.example.com</code>:Subject Alternative Name (SAN) 拡張を使用して、証明書が <code>cas.example.com</code> という DNS 名を持つことを示します。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>各質問の回答は以下とします。(テストサーバーなので、かなり適当な回答です。公式ドキュメントがこうなっていました。)</p>\n<p>cas.example.com</p>\n<p>Test</p>\n<p>Test</p>\n<p>Test</p>\n<p>Test</p>\n<p>US</p>\n<p>はい</p>\n<p>↑</p>\n<p><span style=\"color: red;\">最後の「はい」は日本語 Ubuntu の場合、「Yes」ではなくて、日本語入力で「はい」を入力する必要がありました。</span></p>\n</blockquote>\n\n<br />\n\n\"`The certificate exported out of your keystore needs to also be imported into the Java platform’s global keystore:`\"  \n「`キーストアからエクスポートされた証明書は、Java プラットフォームのグローバル キーストアにもインポートする必要があります。`」  \n(公式ドキュメント)  \nということですので、グローバル キーストアにもインポートします。\n\n<br />\n\nまずは、`JAVA_HOME` 環境変数を設定します。  \nいろいろな方法があると思いますが、ここでは、全ユーザー有効とします。\n\n```shellsession\n# echo 'export JAVA_HOME=$(dirname $(dirname $(readlink -f $(which java))))' >> /etc/profile\n# source /etc/profile\n# echo $JAVA_HOME\n/usr/lib/jvm/java-11-openjdk-amd64\n```\n\n<br />\n\n証明書をファイル(/etc/cas/config/cas.crt)にエクスポートします。\n\n```shellsession\n# mkdir /etc/cas/config\n# keytool -export -file /etc/cas/config/cas.crt -keystore /etc/cas/thekeystore -alias cas\n```\n\n<br />\n\nエクスポートした証明書をグローバル キーストアにインポートします。\n\n```shellsession\n# keytool -import -file /etc/cas/config/cas.crt -alias cas -keystore $JAVA_HOME/lib/security/cacerts\n```\n\n<blockquote class=\"warn\">\n<p>このとき、パスフレーズの入力を求められますが、先ほど設定した <code>xxxxxx</code> ではありません。</p>\n<p>グローバル キーストアのパスフレーズです。</p>\n<p>環境によって異なると思いますが、今回の場合、設定していないため、デフォルトの <code>changeit</code> でした。</p>\n</blockquote>\n\n<br />\n\nビルド後起動してきたアプリケーションサーバーがキーストア /etc/cas/thekeystore を参照して、これのパスワードが `xxxxxx` のため、これをあらかじめ設定しておきます。\n\n```shellsession\n# vi /etc/cas/config/cas.properties\n```\n\n```properties:/etc/cas/config/cas.properties\nserver.ssl.key-store-password=xxxxxx\nserver.ssl.key-password=xxxxxx\n```\n\n設定しない場合、デフォルトの `changeit` で動作しようとするため、以下のエラーになり、起動しません。  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Caused by: java.lang.IllegalArgumentException: keystore password was incorrect</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Caused by: java.io.IOException: keystore password was incorrect</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Caused by: java.security.UnrecoverableKeyException: failed to decrypt safe contents entry: javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption.</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">&gt; Task :webapp:cas-server-webapp-tomcat:bootRun FAILED</span>\n\n<br />\n\n準備が完了したため、ビルドします。\n\n```shellsession\n# cd /opt/cas-server\n# cd webapp/cas-server-webapp-tomcat\n# ../../gradlew build bootRun --parallel --configure-on-demand --build-cache --stacktrace\n```\n\n<blockquote class=\"info\">\n<p><code>bootRun</code> タスクは Spring Boot アプリケーションを起動します。</p>\n<p><code>--stacktrace</code>:ビルドが失敗した場合にスタックトレースを出力します。</p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>公式ドキュメントの場合、<code>--offline</code> オプションがありますが、以下のエラーで止まるため、<code>--offline</code> オプション無しです。</p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">&gt; Task :webapp:cas-server-webapp-tomcat:checkstyleTest FAILED</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">FAILURE: Build failed with an exception.</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">* What went wrong:</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">Execution failed for task ':webapp:cas-server-webapp-tomcat:checkstyleTest'.</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">&gt; Could not resolve all files for configuration ':webapp:cas-server-webapp-tomcat:checkstyle'.</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\"> &gt; Could not resolve com.puppycrawl.tools:checkstyle:10.3.1.</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\"> Required by:</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\"> project :webapp:cas-server-webapp-tomcat</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\"> &gt; No cached version of com.puppycrawl.tools:checkstyle:10.3.1 available for offline mode.</span></p>\n</blockquote>\n\n<br />\n\n以下のような出力になったら、ビルド&起動成功です。\n\n```shellsession\n  ____  _____    _    ______   __\n |  _ \\| ____|  / \\  |  _ \\ \\ / /\n | |_) |  _|   / _ \\ | | | \\ V /\n |  _ <| |___ / ___ \\| |_| || |\n |_| \\_\\_____/_/   \\_\\____/ |_|\n\n>\n2024-02-10 17:59:53,009 INFO [org.apereo.cas.web.CasWebApplicationReady] - <>\n2024-02-10 17:59:53,009 INFO [org.apereo.cas.web.CasWebApplicationReady] - <Ready to process requests @ [2024-02-10T08:59:53.002Z]>\n2024-02-10 18:00:22,995 INFO [org.apereo.cas.ticket.registry.DefaultTicketRegistryCleaner] - <[0] expired tickets removed.>\n<============-> 99% EXECUTING [2m 59s]\n> IDLE\n> IDLE\n> IDLE\n> :webapp:cas-server-webapp-tomcat:bootRun\n```\n\nとりあえず、アクセスしてみます。\n\n<br />\n\n**URL:** `https://cas.example.com:8443/cas/`  \n**User:** <span style=\"color: red;\"><strong><code>casuser</code></strong></span>  \n**Password:** <span style=\"color: red;\"><strong><code>Mellon</code></strong></span>  \nです。\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image3.png\" alt=\"cas.example.com:8443/cas エントリー画面\" width=\"862\" height=\"759\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image4.png\" alt=\"cas.example.com:8443/cas ログイン後\" width=\"1223\" height=\"746\" loading=\"lazy\"></a>\n\n<br />\n\nOK!\n\n<br />\n\n# Build Aliases\n\n`bc` と入力するだけで再ビルド&bootRun のコマンドエイリアス(ショートカット)を作成します。\n\n<blockquote class=\"info\">\n<p><code>cas</code> と入力すると、<code>cd /opt/cas-server</code> の意味のエイリアスも含まれます。  </p>\n<p><code>bci</code> もありますが、<code>bci</code> は、今回使いません。</p>\n</blockquote>\n\n```shellsession\n# vi ~/.bashrc\n```\n\n```shellsession:~/.bashrc\n# Adjust the cas alias to the location of cas project folder\nalias cas='cd /opt/cas-server'\n\n# Run CAS with module selections\n# $> bc oidc,gauth\nfunction bc() {\n  clear\n  cas\n  cd webapp/cas-server-webapp-tomcat\n  casmodules=\"$1\"\n  if [ ! -z \"$casmodules\" ] ; then\n    echo \"Loading CAS Modules: ${casmodules}\"\n  fi\n\n  # Could also use: gm -b ./build.gradle\n  ../../gradlew build bootRun \\\n    --configure-on-demand --build-cache \\\n    --parallel -x test -x javadoc -x check -DenableRemoteDebugging=true \\\n    --stacktrace -DskipNestedConfigMetadataGen=true \\\n    -DremoteDebuggingSuspend=false \\\n    -DcasModules=${casmodules}\n}\n\n# Install JARs/WARs for use with a CAS overlay project\nalias bci='clear; cas; \\\n    ./gradlew clean build publishToMavenLocal \\\n    --configure-on-demand \\\n    --build-cache --parallel \\\n    -x test -x javadoc -x check --stacktrace \\\n    -DskipNestedConfigMetadataGen=true \\\n    -DskipBootifulArtifact=true'\n```\n\n```shellsession\n# source ~/.bashrc\n```\n\n<blockquote class=\"info\">\n<p><code>-DenableRemoteDebugging=true</code>:リモートデバッグを有効にします。</p>\n<p><code>-DskipNestedConfigMetadataGen=true</code>:ネストした設定メタデータの生成をスキップします。</p>\n<p><code>-DremoteDebuggingSuspend=false</code>:リモートデバッグが有効になっている場合でも、JVM の起動を一時停止しません。</p>\n<p><code>-DcasModules=${casmodules}</code>:casModules プロパティを <code>${casmodules}</code> 環境変数の値に設定します。これは、ビルドする CAS モジュールを指定するためのものです。<span style=\"color: red;\">この後の作業で重要な意味を持ちます。</span></p>\n</blockquote>\n\n<br />\n\n# Shibboleth SP 構築\n\nとりあえず、CAS Server の件は一旦置いといて、認証をかけたいアプリケーション側 Apache & PHP & Shibboleth SP の環境を構築します。  \nここは、主題ではないため、細かい説明は端折ります。\n\n<blockquote class=\"warn\">\n<p>アプリケーションの URL は、</p>\n<p><code>https://shibapp.example.com/info.php</code></p>\n<p>とします。</p>\n</blockquote>\n\n```shellsession\n# apt -y update\n# apt -y install apache2\n# apt -y install libapache2-mod-shib\n# a2enmod shib\nModule shib already enabled\n# a2enmod ssl\n# a2ensite default-ssl\n# openssl genrsa -aes128 2048 > server.key\n#  openssl req -new -key server.key > server.csr\nEnter pass phrase for server.key:\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) [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) []:shibapp.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# 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/shib.crt\n# cp -p server.key /etc/ssl/private/shib.key\n# chmod 400 /etc/ssl/private/shib.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/shib.crt\n    SSLCertificateKeyFile /etc/ssl/private/shib.key\n# apt -y install php-common libapache2-mod-php php-cli\n# vi /var/www/html/info.php\n<?php\n  phpinfo();\n# vi /etc/hosts\n127.0.0.1\tshibapp.example.com\n```\n\n<br />\n\n`/info.php` と php にアクセスに来たら、認証がかかるように設定します。\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@shibapp.example.com\n\t\tServerName shibapp.example.com\n\t\tDocumentRoot /var/www/html\n\n\t\tSSLEngine on\n\t\tSSLCertificateKeyFile /etc/ssl/private/shib.key\n\t\tSSLCertificateFile /etc/ssl/certs/shib.crt\n\t\t<Files *.php>\n\t\t\tAuthtype shibboleth\n\t\t\tShibRequireSession On\n\t\t\trequire valid-user\n\t\t</Files>\n\t</VirtualHost>\n\t<VirtualHost _default_:443>\n...(既存設定)\n```\n\n<br />\n\nShibboleth SP の設定をします。  \nここでは、entityID(SP - IdP 間のお互いの識別子)と IdP メタデータの最低限必要な設定だけします。\n\n```shellsession\n# cp -p /etc/shibboleth/shibboleth2.xml /etc/shibboleth/shibboleth2.xml.org\n# vi /etc/shibboleth/shibboleth2.xml\n13     <ApplicationDefaults entityID=\"https://sp.example.org/shibboleth\"\n↓\n13     <ApplicationDefaults entityID=\"https://shibapp.example.com/shibboleth\"\n\n34             <SSO entityID=\"https://idp.example.org/idp/shibboleth\"\n↓\n34             <SSO entityID=\"https://cas.example.com:8443/saml/idp\"\n\n68         <!--\n69         <MetadataProvider type=\"XML\" validate=\"true\" path=\"partner-metadata.xml\"/>\n70         -->\n↓\n68\n69         <MetadataProvider type=\"XML\" validate=\"true\" path=\"/etc/cas/saml/idp-metadata.xml\"/>\n70\n```\n\n<blockquote class=\"warn\">\n<p><code>SSO entityID=\"https://cas.example.com:8443/saml/idp\"</code> は、CAS の SAML IdP エンティティ ID です。この後 CAS 側に設定します。</p>\n<p><code>/etc/cas/saml/idp-metadata.xml</code> は、IdP(CAS Server)側のメタデータ(XML)です。とりあえず、設定だけして、この後置きます。</p>\n</blockquote>\n\n<br />\n\n```shellsession\n# systemctl restart shibd\n# systemctl restart apache2\n```\n\n<br />\n\n# CAS Server SAML 設定\n\nCAS Server の設定を変更して、準備します。\n\n```shellsession\n# vi /etc/cas/config/cas.properties\n```\n\n<br />\n\n```properties:/etc/cas/config/cas.properties\nserver.ssl.key-store-password=xxxxxx\nserver.ssl.key-password=xxxxxx\n# サービスレジストリ(SPの設定)を JSON ファイルから初期化するかどうかを指定\n# true:cas.service-registry.json.location で指定された JSON ファイルからサービス定義を読み込み\n# false:従来の XML ファイルからサービス定義を読み込み\ncas.service-registry.core.init-from-json=true\n# cas.service-registry.json.location で指定された JSON ファイルに変更があった場合に、自動的にサービス定義を更新するかどうかを指定\n# true:ファイル変更を監視し、サービス定義を更新\n# false:ファイル変更を監視せず、サービス定義は手動で更新\ncas.service-registry.json.watcher-enabled=true\n# JSON 形式でサービス定義を記述したファイルの場所を指定\ncas.service-registry.json.location=file:/etc/cas/services\n# CAS の SAML IdP エンティティ ID を指定\ncas.authn.saml-idp.core.entity-id=https://cas.example.com:8443/saml/idp\n# CAS Server の完全な URL を指定\ncas.server.name=https://cas.example.com:8443\n# CAS Server の URL プレフィックスを指定\ncas.server.prefix=${cas.server.name}/cas\n# CAS Server のスコープ(ドメイン名)を指定\ncas.server.scope=example.com\n```\n\n<span style=\"color: red;\"><strong>CAS Server と CAS Modules の一つ saml-idp をビルドして起動します。</strong></span>  \n<span style=\"color: red;\"><strong>このとき、`bc` ではなく、`bc saml-idp` で起動します。</strong></span>  \n<span style=\"color: red;\"><strong>これは何を意味しているかというと、</strong></span>  \n<span style=\"color: red;\"><strong>org.apereo.cas:cas-server-support-saml-idp</strong></span>  \n<span style=\"color: red;\"><strong>をローカルのソースコードからビルドして組み込むという意味です。</strong></span>  \n<span style=\"color: red;\"><strong>指定するのは、</strong></span>  \n<span style=\"color: red;\"><strong>org.apereo.cas:cas-server-support-[サブプロジェクト名プレフィックス部分]</strong></span>  \n<span style=\"color: red;\"><strong>の [サブプロジェクト名プレフィックス部分] で、</strong></span>  \n<span style=\"color: red;\"><strong>例えば、cas-server-support-ldap</strong></span>  \n<span style=\"color: red;\"><strong>もビルドしたい場合、</strong></span>  \n<span style=\"color: red;\"><strong>`bc saml-idp,ldap` です。</strong></span>\n\n<br />\n\n<span style=\"color: red;\"><strong>なお、公式ドキュメントの手順では、以下のように build.gradle を書き換えて、`bc` ですが、この場合、ローカルソースコードが使われずにビルドされます。そのため、一生懸命 LOGGER でログ出力を増やしたりしても反映されません。</strong></span>  \n<span style=\"color: red;\"><strong>今回、下記は実施しません。</strong></span>\n\n```shellsession\n# cd /opt/cas-server\n# vi build.gradle\n```\n\n```java:/opt/cas-server/build.gradle\n// (注意:今回実施しない)\n// 略\nbuildscript {\n    repositories {\n        mavenCentral()\n        gradlePluginPortal()\n        maven {\n            url \"https://repo.spring.io/milestone\"\n            mavenContent { releasesOnly() }\n        }\n// ここから\n        maven {\n            mavenContent { releasesOnly() }\n            url \"https://build.shibboleth.net/maven/releases/\"\n        }\n// ここまで追加\n    }\n// 略\n    dependencies {\n// ここから\n        implementation \"org.apereo.cas:cas-server-support-saml-idp:${project.'version'}\"\n// ここまで追加\n\n        implementation libraries.aspectj\n// 略\n```\n\n<br />\n\n(CTRL + C で停止済みとします。)\n\n```shellsession\n# bc saml-idp\n```\n\nここで、  \n`2024-02-11 12:15:19,131 INFO [org.apereo.cas.support.saml.idp.metadata.generator.BaseSamlIdPMetadataGenerator] - <Creating SAML2 metadata for identity provider...>`  \nという出力があった後、  \n/etc/cas/saml/idp-metadata.xml  \nが作成されます。\n\n<blockquote class=\"warn\">\n<p>本来、この後、idp-metadata.xml を SP 側に送り込まないといけないと思いますが、今回、SP(Shibboleth)も同居しているため、Shibboleth でそのまま /etc/cas/saml/idp-metadata.xml を読み込むように設定しています。</p>\n</blockquote>\n\n<br />\n\n/etc/cas/saml/idp-metadata.xml  \nが作成されて、  \n`READY` 表示があったら、成功ですが、  \nSP の情報を CAS Server に登録していないため、一旦、CTRL + C で止めます。\n\n<br />\n\n# cas-management\n\nSP の情報を CAS Server に登録します。  \n/etc/cas/services/*.json に書くのですが、何をどう書けば良いか分かりませんので、  \ncas-management(`https://apereo.github.io/cas-management/6.6.x/index.html`)を利用し、json を生成します。\n\n<blockquote class=\"info\">\n<p>CAS(Central Authentication Service)と CAS Management Web Application は別々の独立したアプリケーションです。</p>\n<p>CAS Management は CAS のサービスを管理するための管理用インターフェースであり、CAS サーバーの運用能力は CAS Management のデプロイ状態に依存しません。</p>\n<p>つまり、管理画面をオフラインにしたり、完全に削除したりしても、CAS サーバー自体の運用には影響がないですが、サービスレジストリの管理や操作を行うには、別の方法を検討する必要があります。</p>\n</blockquote>\n\n```shellsession\n# cd /opt\n# git clone --depth=1 --single-branch --branch=v6.6.4 https://github.com/apereo/cas-management.git cas-management\n# cd /opt/cas-management\n# ./install.sh\n```\n\n<blockquote class=\"warn\">\n<p>バージョンを指定しないと、CAS 7.x.x 用のものがダウンロードされて、install.sh が存在しません。</p>\n</blockquote>\n\n<br />\n\nビルドに成功したら、CAS Server のときと同じように、キーストアのパスワードを設定しておきます。\n\n```shellsession\n# vi /etc/cas/config/management.properties\n```\n\n```properties:/etc/cas/config/management.properties\nserver.ssl.key-store-password=xxxxxx\nserver.ssl.key-password=xxxxxx\n```\n\nこれを行わないと、以下のエラーになり、cas-management が起動しません。  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Caused by: org.apache.catalina.LifecycleException: Protocol handler start failed</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Caused by: java.lang.IllegalArgumentException: keystore password was incorrect</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Caused by: java.io.IOException: keystore password was incorrect</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Caused by: java.security.UnrecoverableKeyException: failed to decrypt safe contents entry: javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption.</span>\n\n<br />\n\nさらに、そのまま起動すると、CAS Server のポート 8443 と被るため、ポートを変更します。\n\n```properties:/etc/cas/config/management.properties\nmgmt.server-name=https://localhost:9443\nserver.port=9443\n```\n\n<br />\n\nCAS Management Web App にアクセスしたとき最初に CAS Server を利用した認証があるため、CAS Server にリダイレクトされるのですが、  \n`https://cas.example.org:8443/cas/login`(.com ではなく、デフォルトの.org)  \nにリダイレクトされるため、CAS Server についての設定も追加します。  \nこのとき、サービスレジストリ(SP についての *.json を置く場所のこと)も `/etc/cas/services` に設定しておきます。\n\n```properties:/etc/cas/config/management.properties\ncas.server.name=https://cas.example.com:8443\ncas.server.prefix=${cas.server.name}/cas\ncas.service-registry.json.location=file:/etc/cas/services\n```\n\n<br />\n\nCAS Management Web App についての認証設定を作成します。\n\n<blockquote class=\"warn\">\n<p><code>cas.service-registry.json.location=file:/etc/cas/services</code></p>\n<p>の設定が影響して、内部デフォルトから *.json 読み取りに変更されるため、自分で設定しておかないといけないです。</p>\n</blockquote>\n\n```shellsession\n# mkdir /etc/cas/services\n# vi /etc/cas/services/casmanagement-001.json\n```\n\n```json:/etc/cas/services/casmanagement-001.json\n{\n  \"@class\": \"org.apereo.cas.services.CasRegisteredService\",\n  \"serviceId\": \"https://localhost:9443/cas-management/.*\",\n  \"name\": \"CAS Management Web App\",\n  \"id\": 10000001\n}\n```\n\n<br />\n\nCAS Server を起動します。\n\n```shellsession\n# bc saml-idp\n```\n\n<br />\n\n続いて、CAS Management Web ダッシュボードを build & bootRun します。\n\n```shellsession\n# cd /opt/cas-management\n# cd webapp/cas-mgmt-webapp-tomcat\n# ../../gradlew build && ../../gradlew bootRun --configure-on-demand --build-cache --parallel --scan --stacktrace -x check -x test -x javadoc -DskipErrorProneCompiler=true -DbuildDev=true -DskipClientBuild=true\n```\n\n<br />\n\n`READY` が表示されたら、`https://localhost:9443/cas-management` にアクセスします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image5.png\" alt=\"cas-management ログイン\" width=\"900\" height=\"820\" loading=\"lazy\"></a>\n\n**User:** <span style=\"color: red;\"><strong><code>casuser</code></strong></span>  \n**Password:** <span style=\"color: red;\"><strong><code>Mellon</code></strong></span>  \nでログインします。\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image6.png\" alt=\"cas-management ログイン後\" width=\"1082\" height=\"828\" loading=\"lazy\"></a>\n\n<br />\n\n**SAML Services** をクリックします。  \n右上の **+** ボタンをクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image7.png\" alt=\"SAML Services→+ボタンクリック\" width=\"1124\" height=\"278\" loading=\"lazy\"></a>\n\n<br />\n\n**+ New Service** をクリックします。\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image8.png\" alt=\"+ New Serviceクリック\" width=\"1126\" height=\"435\" loading=\"lazy\"></a>\n\n<br />\n\n**Basics** タブで、  \n**Service Type**: `SAML2 Service Provider`  \n**Entity Id**: `https://shibapp.example.com/shibboleth`  \n**Service Name**: `shibapp`  \nと入力します。\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image9.png\" alt=\"Basicsタブ設定\" width=\"1126\" height=\"797\" loading=\"lazy\"></a>\n\n<br />\n\n**Metadata** タブで、  \n**Metadata Location**: `https://shibapp.example.com/Shibboleth.sso/Metadata`  \nと入力し、右上の Save ボタンをクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image10.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image10.png\" alt=\"Metadataタブ設定\" width=\"1129\" height=\"799\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image11.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image11.png\" alt=\"Save ボタンをクリック直後\" width=\"1128\" height=\"805\" loading=\"lazy\"></a>\n\n<br />\n\n実際に確認してみます。\n\n```shellsession\n# find /etc/cas/services/\n/etc/cas/services/\n/etc/cas/services/shibapp-1707985312253.json\n# cat /etc/cas/services/shibapp-1707985312253.json\n```\n\n```json:/etc/cas/services/shibapp-1707985312253.json\n{\n  @class: org.apereo.cas.support.saml.services.SamlRegisteredService\n  serviceId: https://shibapp.example.com/shibboleth\n  name: shibapp\n  id: 1707985312253\n  metadataLocation: https://shibapp.example.com/Shibboleth.sso/Metadata\n  skipGeneratingSubjectConfirmationNameId: false\n  signingCredentialType: BASIC\n}\n```\n\n追加されました!\n\n<blockquote class=\"info\">\n<p>この .json は手動で作成しても同じことです。</p>\n<p>ファイル名は、[英数字]-[数字].json が推奨されているようです。</p>\n</blockquote>\n\n<br />\n\n# Shibboleth SP SAML 認証確認\n\nさて、ここで、SP, IdP ともに準備完了なので、  \n`https://shibapp.example.com/info.php`  \nにアクセスしたいところですが、まだやることがあります!\n\n<br />\n\n`https://shibapp.example.com/info.php` が自己署名証明書のため、  \nCAS Server → `https://shibapp.example.com/Shibboleth.sso/Metadata`  \nのアクセスに失敗します。  \n以下のエラーになります。  \n<span style=\"color: #e70500;background-color: #ffebe7;\">2024-02-10 16:31:34,340 ERROR [org.apereo.cas.util.HttpUtils] - <SSL error accessing: [https://shibapp.example.com/Shibboleth.sso/Metadata]</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\"> Alert.java:createSSLException:131</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\"> TransportContext.java:fatal:360</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\"> TransportContext.java:fatal:303</span>\n\n<br />\n\nエラー時の画面表示は以下です。  \n<span style=\"color: #e70500;background-color: #ffebe7;\">アプリケーションは CAS を使う権限がありません</span>\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image12.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image12.png\" alt=\"アプリケーションは CAS を使う権限がありません\" width=\"1204\" height=\"300\" loading=\"lazy\"></a>\n\n<br />\n\n/etc/ssl/certs/shib.crt にある証明書を Java のデフォルトのキーストアにインポートして、Java から見て信頼済みのサイトとします。\n\n```shellsession\n# keytool -import -trustcacerts -cacerts -storepass changeit -noprompt -alias shibboleth -file /etc/ssl/certs/shib.crt\n```\n\n<br />\n\n`https://shibapp.example.com/info.php` にアクセスします。  \nこのとき、まず、CAS Server の認証画面にリダイレクトされるため、  \n**User:** <span style=\"color: red;\"><strong><code>casuser</code></strong></span>  \n**Password:** <span style=\"color: red;\"><strong><code>Mellon</code></strong></span>  \nでログインします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image13.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image13.png\" alt=\"CAS Server の認証画面にリダイレクト\" width=\"1216\" height=\"865\" loading=\"lazy\"></a>\n\n<br />\n\n認証後、php にアクセスできて、アプリケーションのコンテンツが表示されます!\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image14.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image14.png\" alt=\"アプリケーションのコンテンツ表示\" width=\"1214\" height=\"349\" loading=\"lazy\"></a>\n\n<br />\n\nこれで第一弾の普通の SAML 認証は完了です。\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image1.png\" alt=\"普通の SAML 認証 Shibboleth(SAML SP) 図\" width=\"801\" height=\"391\" loading=\"lazy\"></a>\n\n<br />\n\n続いて、CAS を介して Azure AD 認証を実現する手順に入ります。  \n完成後のイメージは以下です。  \n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image2.png\" alt=\"CAS を介して Azure AD 認証 図\" width=\"1031\" height=\"471\" loading=\"lazy\"></a>\n\n<br />\n\n# Azure AD 連携\n\n先ほど、CAS Server のログイン情報でアプリケーションにログインしましたが、Azure AD で認証できるようにします。  \nこれが、`https://apereo.github.io/cas/6.6.x/integration/Delegate-Authentication-SAML.html` で、  \n\"`Delegated Authentication w/ SAML2`\"(w/ は、with の省略)と言っているやつです。\n\n<br />\n\n## Azure AD 設定\n\nCAS Server の設定変更が必要ですが、まず、Azure AD の SAML エンタープライズ アプリケーション登録を行います。\n\n<br />\n\n別の SAML の記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/keycloak-saml-azuread#h2-titile-28\" target=\"_blank\">Ubuntu 22 に Keycloak 22 をインストールして、Identity providers=Azure AD で SAML - IdP:Azure AD</a>」と同じ手順のため、省略します。\n\n<br />\n\n異なるのは、  \n**識別子 (エンティティ ID)**、<strong>応答 URL (Assertion Consumer Service URL)</strong> 部分で、  \n**識別子 (エンティティ ID)**:`https://cas.example.com:8443`  \n**応答 URL (Assertion Consumer Service URL)**:`https://cas.example.com:8443/cas/login?client_name=SAML2Client`  \nです。\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image15.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image15.png\" alt=\"Azure AD SAML エンタープライズ アプリケーション登録\" width=\"809\" height=\"1113\" loading=\"lazy\"></a>\n\n<br />\n\nここで、  \n**アプリのフェデレーション メタデータ URL** と書かれている URL から XML ファイルをダウンロードします。  \nここではそれを /home/admin/SAML2Client.xml として、配置したものとします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image16.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image16.png\" alt=\"アプリのフェデレーション メタデータ URL\" width=\"807\" height=\"205\" loading=\"lazy\"></a>\n\n<br />\n\n## Azure AD のメタデータ配置\n\nIdP(Azure AD)のメタデータ(XML)を配置します。\n\n```shellsession\n# cp /home/admin/SAML2Client.xml /etc/cas/SAML2Client.xml\n```\n\n<br />\n\n## CAS 設定\n\n以下の設定をします。\n\n```shellsession\n# vi /etc/cas/config/cas.properties\n```\n\n<blockquote class=\"warn\">\n<p>/etc/cas/config/sp-metadata.xml</p>\n<p>がまだ存在しませんが、この後、自動生成しますので、とりあえず、パスだけ書きます。</p>\n</blockquote>\n\n```properties:/etc/cas/config/cas.properties\n# SAML認証に使用するキーストアのパスワード\ncas.authn.pac4j.saml[0].keystore-password=pac4j-demo-passwd\n# プライベートキーのパスワード\ncas.authn.pac4j.saml[0].private-key-password=pac4j-demo-passwd\n# サービスプロバイダのエンティティID(今回の場合は、CAS Server。Azure ADから見たサービスプロバイダ。一般的にはURLを記載)\ncas.authn.pac4j.saml[0].service-provider-entity-id=https://cas.example.com:8443\n# キーストアへのパス\ncas.authn.pac4j.saml[0].keystore-path=/etc/cas/config/samlKeystore.jks\n# サービスプロバイダのメタデータへのパス\ncas.authn.pac4j.saml[0].service-provider-metadata-path=/etc/cas/config/sp-metadata.xml\n# アイデンティティプロバイダのメタデータへのパス\ncas.authn.pac4j.saml[0].identity-provider-metadata-path=/etc/cas/SAML2Client.xml\n# クライアント名(IdP選択ボタンに表示される名前)\ncas.authn.pac4j.saml[0].client-name=SAML2Client\n# SAMLRequestにNameQualifierを含めるかどうか(false: 含めない)\ncas.authn.pac4j.saml[0].use-name-qualifier=false\n```\n\n<span style=\"color: red;\"><strong>ここで、</strong></span>  \n<span style=\"color: red;\"><strong>`cas.authn.pac4j.saml[0].use-name-qualifier=false`</strong></span>  \n<span style=\"color: red;\"><strong>に関しては、重要な意味を持ちます。</strong></span>  \n<span style=\"color: red;\"><strong>なぜか、Azure AD に NameQualifier を含めた SAMLRequest を送信すると、以下のエラーになります。</strong></span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">AADSTS7500525: There was an XML error in the SAML message at line 2, position 709. Verify that the XML content of the SAML messages conforms to the SAML protocol specifications.</span>\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image17.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image17.png\" alt=\"AADSTS7500525 エラー\" width=\"1306\" height=\"808\" loading=\"lazy\"></a>\n\n<br />\n\n```xml:エラーのSAMLRequest\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<saml2p:AuthnRequest AssertionConsumerServiceURL=\"https://cas.example.com:8443/cas/login?client_name=SAML2Client\" AttributeConsumingServiceIndex=\"0\" Destination=\"https://login.microsoftonline.com/********-****-****-****-************/saml2\" ForceAuthn=\"false\" ID=\"_***************************************\" IsPassive=\"false\" IssueInstant=\"2024-02-16T13:54:23.324Z\" ProtocolBinding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Version=\"2.0\"\n\txmlns:saml2p=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n\t<saml2:Issuer Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:entity\" NameQualifier=\"https://cas.example.com:8443\"\n\t\txmlns:saml2=\"urn:oasis:names:tc:SAML:2.0:assertion\">https://cas.example.com:8443\n\t</saml2:Issuer>\n</saml2p:AuthnRequest>\n```\n\n```xml:正しいSAMLRequest\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<saml2p:AuthnRequest AssertionConsumerServiceURL=\"https://cas.example.com:8443/cas/login?client_name=SAML2Client\" AttributeConsumingServiceIndex=\"0\" Destination=\"https://login.microsoftonline.com/********-****-****-****-************/saml2\" ForceAuthn=\"false\" ID=\"_***************************************\" IsPassive=\"false\" IssueInstant=\"2024-02-16T13:54:23.324Z\" ProtocolBinding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Version=\"2.0\"\n\txmlns:saml2p=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n\t<saml2:Issuer Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:entity\"\n\t\txmlns:saml2=\"urn:oasis:names:tc:SAML:2.0:assertion\">https://cas.example.com:8443\n\t</saml2:Issuer>\n</saml2p:AuthnRequest>\n```\n\n<br />\n\n## CAS 起動\n\n<span style=\"color: red;\"><strong>`bc saml-idp,pac4j-webflow` で起動します。</strong></span>  \n<span style=\"color: red;\"><strong>すなわち、`org.apereo.cas:cas-server-support-pac4j-webflow` を有効にして、ローカルソースコードをビルドします。</strong></span>\n\n```shellsession\n# bc saml-idp,pac4j-webflow\n```\n\n<br />\n\nアプリケーション `https://shibapp.example.com/info.php` にアクセスします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image18.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image18.png\" alt=\"SAML2CLIENT表示\" width=\"1058\" height=\"835\" loading=\"lazy\"></a>\n\n<br />\n\nなんか、「SAML2CLIENT」って出てきました!\n\n<br />\n\n<span style=\"color: red;\"><strong>なお、この瞬間、/etc/cas/config/sp-metadata.xml が生成されます。</strong></span>\n\n<blockquote class=\"warn\">\n<p>pac4j-webflow を有効にしないと、「SAML2CLIENT」は出てこず、sp-metadata.xml も生成されません。</p>\n</blockquote>\n\n<br />\n\n## IdP 選択\n\nユーザー名とパスワードを入力すると、今まで通り、IdP = CAS Server でログインになります。  \nIdP = Azure AD でログインしたいので、**SAML2CLIENT** ボタンを押します。\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image19.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image19.png\" alt=\"SAML2CLIENTボタンクリック\" width=\"1054\" height=\"223\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image20.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image20.png\" alt=\"Azure AD 認証開始\" width=\"1139\" height=\"834\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image14.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image14.png\" alt=\"アプリケーションのコンテンツ表示\" width=\"1214\" height=\"349\" loading=\"lazy\"></a>\n\n<br />\n\nAzure AD 認証ヨシっ!\n\n<br />\n\n以上!\n\n<br />\n\nといきたいところですが、Azure AD 一択の場合、いちいち「SAML2CLIENT」ボタンを押すのがめんどくさいです。\n\n<br />\n\nこれをなくす設定があります。\n\n<br />\n\n```properties:/etc/cas/config/cas.properties\ncas.authn.pac4j.saml[0].auto-redirect-type=CLIENT\n```\n\nもしくは、\n\n<br />\n\n```properties:/etc/cas/config/cas.properties\ncas.authn.pac4j.saml[0].auto-redirect-type=SERVER\n```\n\nです。\n\n<br />\n\nどちらも、「SAML2CLIENT」ボタンを押す必要がなく、Azure AD 認証に遷移します。  \nどう違うかというと、「お待ちください」的な画面が一瞬表示されるか、全く画面が表示されないかの違いです。\n\n<br />\n\n全く画面が表示されないのは、  \n`cas.authn.pac4j.saml[0].auto-redirect-type=SERVER`  \nです。  \nCAS Server → `https://login.microsoftonline.com/` の直接通信ができない場合、  \n`cas.authn.pac4j.saml[0].auto-redirect-type=CLIENT`  \nを設定することになると思います。\n\n<br />\n\n**`SERVER`**\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image21.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image21.png\" alt=\"auto-redirect-type=SERVER 図\" width=\"1031\" height=\"471\" loading=\"lazy\"></a>\n\n<br />\n\n**`CLIENT`**\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image22.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image22.png\" alt=\"auto-redirect-type=CLIENT 図\" width=\"1031\" height=\"471\" loading=\"lazy\"></a>\n\n<blockquote class=\"info\">\n<p>JavaScript の <code>onLoad</code> で SAML2CLIENT ボタンを押したことにしているから自動で遷移します。</p>\n</blockquote>\n\n<br />\n\n# おまけ(IntelliJ IDEA でのデバッグ)\n\n目的は達成しましたが、ソースコードをビルドしてデバッグポートを開いているのだから、IntelliJ IDEA Community でのデバッグを行ってみます。\n\n<br />\n\n## IntelliJ インストール\n\nIntelliJ IDEA Community をインストールします。\n\n<blockquote class=\"warn\">\n<p>Ubuntu 22 の場合、デスクトップ(リモートデスクトップではない。)にログインして、Terminal アプリで作業を行う必要があります。</p>\n</blockquote>\n\n```shellsession\n# tar -xzf /home/admin/ideaIC-2023.3.3.tar.gz -C /opt\n# cd /opt/idea-IC-233.14015.106/bin\n# ./idea.sh\n```\n\n<br />\n\nプロジェクトを開くたびに  \n`認証が必要です`  \n`コンピューターへのログインに使用するパスワードが、もはやログインキーリングのパスワードと一致しなくなっています。`  \nと表示されるのがうざいため、表示されなくしておきます。\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image23.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image23.png\" alt=\"認証が必要です\" width=\"538\" height=\"390\" loading=\"lazy\"></a>\n\n<blockquote class=\"alert\">\n<p>この行為の意味が分からない場合は、実施しないでください。<code>認証が必要です</code> は、<strong>キャンセル</strong> クリックで、先に進みます。</p>\n</blockquote>\n\n```shellsession\n# rm /home/admin/.local/share/keyrings/login.keyring\n```\n\nIntelliJ IDEA Community 起動後、  \n`新しいキーリングのパスワード指定`  \nと表示されるため、空白のまま **続行** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image24.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image24.png\" alt=\"新しいキーリングのパスワード指定\" width=\"603\" height=\"479\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image25.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image25.png\" alt=\"続行クリック\" width=\"547\" height=\"339\" loading=\"lazy\"></a>\n\n<br />\n\n## Debug 設定\n\n左上のハンバーガーメニューをクリックして、**Run** → **Edit Configurations...** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image26.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image26.png\" alt=\"Edit Configurations\" width=\"779\" height=\"508\" loading=\"lazy\"></a>\n\n<br />\n\n左上の **+** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image27.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image27.png\" alt=\"+クリック\" width=\"803\" height=\"684\" loading=\"lazy\"></a>\n\n<br />\n\n**Remote JVM Debug** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image28.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image28.png\" alt=\"Remote JVM Debug\" width=\"820\" height=\"687\" loading=\"lazy\"></a>\n\n<br />\n\n**Port:** を `5000` に変更し、**Apply** → **OK** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image29.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image29.png\" alt=\"5000→Apply→OK\" width=\"821\" height=\"685\" loading=\"lazy\"></a>\n\n<br />\n\n## ブレークポイント設置\n\nファイル: /opt/cas-server/api/cas-server-core-api-authentication/src/main/java/org/apereo/cas/authentication/AuthenticationManager.java  \n対象コード: `Authentication authenticate(AuthenticationTransaction authenticationTransaction) throws AuthenticationException;`  \nにブレークポイントを張ります。\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image30.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image30.png\" alt=\"ブレークポイント設置\" width=\"1397\" height=\"1002\" loading=\"lazy\"></a>\n\n<br />\n\n## Debug\n\nCAS Server を `bc saml-idp,pac4j-webflow` で起動します。\n\n<br />\n\n左上のハンバーガーメニューをクリックして、**Run** → **Run** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image31.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image31.png\" alt=\"Run→Run\" width=\"768\" height=\"505\" loading=\"lazy\"></a>\n\n<br />\n\n**Unnamed** → **Debug** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image32.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image32.png\" alt=\"Unnamed→Debug\" width=\"437\" height=\"233\" loading=\"lazy\"></a>\n\n<br />\n\n`https://shibapp.example.com/info.php` にアクセスして、ログインする際に、ブレークポイントで止まります。\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image33.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image33.png\" alt=\"アクセスして、ログイン\" width=\"1057\" height=\"748\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image34.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/image34.png\" alt=\"ブレークポイントで停止\" width=\"1395\" height=\"996\" loading=\"lazy\"></a>\n\n<br />\n\nヨシっ!  \n以上!\n","description":"Apereo CAS Server 6.6.15 で Delegated Authentication with SAML2 を実施して、Azure AD(Microsoft Entra ID) SSO 環境を構築しました。Shibboleth を アプリケーション側の SP として採用しています。その全手順です。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"k4wqlo-wz","createdAt":"2024-02-17T12:02:39.735Z","updatedAt":"2024-02-17T12:02:39.735Z","publishedAt":"2024-02-17T12:02:39.735Z","revisedAt":"2024-02-17T12:02:39.735Z","url":"https://itc-engineering-blog.imgix.net/apereo-cas-azuread/ITC_Engineering_Blog.png","alt":"Apereo CASでDelegated Authentication with SAML2によるAzure AD SSO環境構築","width":1200,"height":630}],"seo_authors":[]},{"id":"php2python","createdAt":"2021-07-20T13:21:57.591Z","updatedAt":"2021-07-20T13:21:57.591Z","publishedAt":"2021-07-20T13:21:57.591Z","revisedAt":"2021-07-20T13:21:57.591Z","title":"php→pythonのトランスパイル","category":{"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},"topics":[{"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":"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":"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":"o9t1ulsh2g","createdAt":"2021-07-03T13:32:00.531Z","updatedAt":"2021-08-31T12:03:24.477Z","publishedAt":"2021-07-03T13:32:00.531Z","revisedAt":"2021-08-31T12:03:24.477Z","topics":"JavaScript","logo":"/logos/JavaScript.png","needs_title":false}],"content":"# はじめに\n詳細は、<a href=\"https://itc-engineering-blog.netlify.app/blogs/83krb8517u\">別記事</a>にありますが、ラズパイでBME280センサから取得した温湿度気圧をWeb画面に表示する wiringpi-php-bme280(GitHub) というのを作りました。  \nこちらは、phpのプログラムですが、同じことがpythonでもできるはずです。  \nそこで、まず、ツールでphp→pythonの変換をして、それを元に開発することにしました。今回は、変換ツール(トランスパイラ)の選定と試した結果を書きたいと思います。\n\n<blockquote class=\"info\">\n<p><strong>【 トランスパイル(transpile) 】</strong></p>\n<p>トランスパイル(transpile)とは、あるプログラミング言語から他のプログラミング言語へと変換(翻訳)することです。変換ツールをトランスパイラと呼びます。JavaScriptのBabelが有名です。</p>\n</blockquote>\n\n<br />\n\n・[結果総評](#anchor1)  \n・[変換元プログラムについて](#anchor2)  \n・[nicolasrod/php2python](#anchor3)  \n・[danleyb2/php2python](#anchor4)  \n・[taichino/php2py](#anchor5)  \n・[おまけ php→js](#anchor6)  \n\n<br />\n\n<blockquote class=\"info\">\n<p>特に言及が無いところでは、root権限で作業していますので、sudoは省略しています。</p>\n</blockquote>\n\n<br />\n\n<a class=\"anchor\" id=\"anchor1\"></a>\n\n# 結果総評\nツールのインストールの仕方、実行方法の前に、結果を先出ししますと、以下になります。  \n\n| リポジトリ | 結果 |\n| ---- | ---- |\n| <a href=\"https://github.com/nicolasrod/php2python\" target=\"_blank\">nicolasrod/php2python</a> | 良いかもしれないが、変換後もツールに依存してしまい、今回の用途で求めていた結果と違う。※変換結果の動作は未検証 |\n| <a href=\"https://github.com/danleyb2/php2python\" target=\"_blank\">danleyb2/php2python</a> | 変換の正確性がいまいち。インストールは簡単。 |\n| <a href=\"https://github.com/taichino/php2py\" target=\"_blank\">taichino/php2py</a> | preg_matchなど関数は変換されない。インストールが難しい。関数以外完璧かというとそうでもない。 |\n  \n<br />\nインストールに難儀しましたが、 <a href=\"https://github.com/taichino/php2py\" target=\"_blank\">taichino/php2py</a> が良いかなという印象です。  \nphpの関数がそのままになるので、 <a href=\"https://github.com/nicolasrod/php2python\" target=\"_blank\">nicolasrod/php2python</a> と見比べながら変換していくと良いかもしれません。  \nただ、これをもってしても結局かなりの人力が必要という印象です。正直 wiringpi-php-bme280 の場合、規模が小さいため、全部人力の方が早かったです。  \n\n<br />\n\n<a class=\"anchor\" id=\"anchor2\"></a>\n\n# 変換元プログラムについて\n以下のphpプログラムで試します。動作内容は、特に意味は無いです。\n\n```php\n<?php\necho \"hello\\n\";\n$dir = \"/path/to\";\nif( !preg_match( \"/\\/$/\", $dir ) ) $dir .= \"/\";\necho $dir . \"\\n\";\n\n$a[] = \"1\";\n$a[] = \"2\";\n$a[] = \"3\";\n$x = array();\nforeach ( $a as $b ) {\n        $x[] = $b . \"b\";\n}\n\nprint_r( $x );\n\n$i = 1;\nif ($i == 0) {\n    echo \"i=0\";\n} elseif ($i == 1) {\n    echo \"i=1\";\n} elseif ($i == 2) {\n    echo \"i=2\";\n}\n\necho \"\\n\";\n\nswitch ($i) {\n    case 0:\n        echo \"i=0\";\n        break;\n    case 1:\n        echo \"i=1\";\n        break;\n    case 2:\n        echo \"i=2\";\n        break;\n}\n\necho \"\\n\";\n\nclass User\n{\n  public $name;\n  private $nickname;\n  public function __construct($name,$nickname){\n    $this->name = $name;\n    $this->nickname = $nickname;\n  }\n}\n\n$user = new User('foo','bar');\necho $user->name;\n```\n\nこれを変換した場合、以下のようになるのが期待されます。(人力変換です。試したツールでは実現しませんでした。)  \n\n```python\n#!/usr/bin/python\n#-*- coding: utf-8 -*-\nimport re\nprint(\"hello\\n\", end='')\ndir = \"/path/to\"\nif not re.match(\"\\/$\", dir):\n  dir+=\"/\"\nprint(dir+\"\\n\", end='')\n\na = []\na.append(\"1\")\na.append(\"2\")\na.append(\"3\")\n\nx = []\nfor b in a:\n    x.append(b + \"b\")\n\nprint(x)\n\ni = 1\nif i==0:\n  print(\"i=0\", end='')\nelif i==1:\n  print(\"i=1\", end='')\nelif i==2:\n  print(\"i=2\", end='')\n\nprint(\"\\n\", end='')\n\nif i==0:\n  print(\"i=0\", end='')\nelif i==1:\n  print(\"i=1\", end='')\nelif i==2:\n  print(\"i=2\", end='')\n\nprint(\"\\n\", end='')\n\nclass User:\n  name=0\n  nickname=0\n  def __init__(self, name, nickname):\n    self.name = name\n    self.nickname = nickname\n\nuser = User(\"foo\", \"bar\")\nprint(user.name, end='')\n```\n\n<br />\n\n**[実行結果]**\n\n```sh\n# php sample.php\nhello\n/path/to/\nArray\n(\n    [0] => 1b\n    [1] => 2b\n    [2] => 3b\n)\ni=1\ni=1\n# python3 sample.py\nhello\n/path/to/\n['1b', '2b', '3b']\ni=1\ni=1\n```\n\n<br />\n\n<a class=\"anchor\" id=\"anchor3\"></a>\n\n# nicolasrod/php2python\nラズベリーパイOSに <a href=\"https://github.com/nicolasrod/php2python\" target=\"_blank\">nicolasrod/php2python</a> をインストールして使ってみます。\n\n<blockquote class=\"warn\">\n<p>検証環境は、</p>\n<p><code>Raspbian GNU/Linux 10 (buster)</code></p>\n<p><code>Python 3.7.3</code></p>\n<p><code>PHP 7.3.29-1</code></p>\n<p>になります。</p>\n<p></blockquote>\n\n<br />\n\n## インストール\nGitHubから php2python-master.zip をダウンロードします。  \n※ここでは、/home/pi に置くものとします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/php2python/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/php2python/image1.png\"  width=\"600px\" alt=\"\" loading=\"lazy\"></a>  \n\n<br />\n\n### phpインストール\nphpをインストールします。\n\n```sh\n# apt update\n# apt upgrade\n```\n※以降基本的にYのため、-yを付けます。-y は、? [Y/n]: のようなときに自動的に y とするオプションです。\n\n```sh\n# apt install -y php\n```\n\n<blockquote class=\"warn\">\n<p><code>apt update</code>と<code>apt upgrade</code>は必要無いかもしれませんが、</p>\n<p><code>apt install -y php</code>で</p>\n<p><span style=\"color: red;\">E: いくつかのアーカイブを取得できません。apt-get update を実行するか --fix-missing オプションを付けて試してみてください。</span></p>\n<p>のエラーになり、今回は必要でした。</p>\n<p></blockquote>\n\n※pythonは最初から入っていました。\n\n<br />\n\n### Composerインストール\nComposerをインストールします。\n\n<blockquote class=\"info\">\n<p><strong>【 Composer 】</strong></p>\n<p>Composerとはphpのライブラリ管理システムです。Node.jsで言うnpm, yarn、Pythonで言うpipのようなものです。</p>\n</blockquote>\n\n<a href=\"https://getcomposer.org/download/\" target=\"_blank\">https://getcomposer.org/download/</a> から composer.phar をダウンロードして、php2python-masterに配置し、インストールします。\n\n※piユーザーで作業するものとします。\n```sh\n# su - pi\n$ unzip php2python-master.zip\n$ cp composer.phar php2python-master/\n$ cd php2python-master\n$ php composer.phar install\n```\n\n<blockquote class=\"info\">\n<p>piユーザー(一般ユーザー)としている理由は、<code>php composer.phar install</code>で以下のようにroot権限でやらない方が良いと怒られるからです。(root権限でインストールはできます。)</p>\n<p><span style=\"color: red;\">Do not run Composer as root/super user! See https://getcomposer.org/root for details</span></p>\n<p><span style=\"color: red;\">Continue as root/super user [yes]?</span></p>\n</blockquote>\n\n<br />\n\n### php2pythonインストール\nREADME.mdに書かれている通りで特に問題無かったです。\n\n```sh\n$ python3 -m pip install -r requirements.txt\n```\n\n<br />\n\n## 動作確認結果\nサンプルプログラム  \n/home/pi/php2python-master/sample/sample.php  \nがあるとします。  \n\n```sh\n$ python3 php2py.py --keep-ast ./sample\n[+] Converting ./sample/sample.php...\n[*] Done!\n```\n\n<blockquote class=\"info\">\n<p><strong>【 AST 】</strong></p>\n<p>--keep-astとすると、変換後の.pyファイルとともに、.astファイルも作成されます。ASTとは、Abstract syntax tree=抽象構文木、つまり、構文解析結果のことです。</p>\n</blockquote>\n\n変換結果を見てみます。\n\n```python\n#!/usr/bin/env python3\n# coding: utf-8\nif '__PHP2PY_LOADED__' not in globals():\n    import os, os.path, sys\n    __compat_layer = os.getenv('PHP2PY_COMPAT', 'php_compat.py')\n    if not os.path.exists(__compat_layer):\n        sys.exit(f'[-] Compatibility layer not found in file {__compat_layer}. Aborting.')\n    # end if\n    with open(__compat_layer) as f:\n        exec(compile(f.read(), '<string>', 'exec'))\n    # end with\n    globals()['__PHP2PY_LOADED__'] = True\n# end if\nimport inspect\nphp_print(\"hello\\n\")\ndir_ = \"/path/to\"\nif (not php_preg_match(\"/\\\\/$/\", dir_)):\n    dir_ += \"/\"\n# end if\nphp_print(dir_ + \"\\n\")\na_[-1] = \"1\"\na_[-1] = \"2\"\na_[-1] = \"3\"\nx_ = Array()\nfor b_ in a_:\n    x_[-1] = b_ + \"b\"\n# end for\nprint_r(x_)\ni_ = 1\nif i_ == 0:\n    php_print(\"i=0\")\nelif i_ == 1:\n    php_print(\"i=1\")\nelif i_ == 2:\n    php_print(\"i=2\")\n# end if\nphp_print(\"\\n\")\nfor case in Switch(i_):\n    if case(0):\n        php_print(\"i=0\")\n        break\n    # end if\n    if case(1):\n        php_print(\"i=1\")\n        break\n    # end if\n    if case(2):\n        php_print(\"i=2\")\n        break\n    # end if\n# end for\nphp_print(\"\\n\")\nclass User():\n    name = Array()\n    nickname = Array()\n    def __init__(self, name_=None, nickname_=None):\n\n\n        self.name = name_\n        self.nickname = nickname_\n    # end def __init__\n# end class User\nuser_ = php_new_class(\"User\", lambda : User(\"foo\", \"bar\"))\nphp_print(user_.name)\n```\n\n文法も関数も置き換わっています。  \nただ独自定義の関数に置き換わっていて、`nicolasrod/php2python`一式必要なようです。  \n今回の用途ではやりすぎ感があるかなと思いました。  \n<br />\nちなみに、作法があるようで、エラーになり、実行はできませんでした。(実行できるようにする方法は調べていません。)\n\n```sh\n$ python3 sample/sample.py\nTraceback (most recent call last):\n  File \"sample/sample.py\", line 10, in <module>\n    exec(compile(f.read(), '<string>', 'exec'))\n  File \"<string>\", line 335, in <module>\nFileNotFoundError: [Errno 2] No such file or directory: 'sample/php_compat.ini'\n```\n\n<br />\n\n<a class=\"anchor\" id=\"anchor4\"></a>\n\n# danleyb2/php2python\nラズベリーパイOSに <a href=\"https://github.com/danleyb2/php2python\" target=\"_blank\">danleyb2/php2python</a> をインストールして使ってみます。\n\n<blockquote class=\"warn\">\n<p>検証環境は、</p>\n<p><code>Raspbian GNU/Linux 10 (buster)</code></p>\n<p><code>Python 3.7.3</code></p>\n<p><code>PHP 7.3.29-1</code></p>\n<p>になります。</p>\n<p></blockquote>\n\n<br />\n\n## インストール\n### phpインストール\nphpをインストールします。\n\n```sh\n# apt update\n# apt upgrade\n```\n※以降基本的にYのため、-yを付けます。-y は、? [Y/n]: のようなときに自動的に y とするオプションです。\n\n```sh\n# apt install -y php\n```\n※pythonは最初から入っていました。\n\n<br />\n\n### php2pythonインストール\n以下のエラーになり、README.mdに書かれている通りではうまくいきませんでした。\n\n```sh\n# pip install convert2php\nLooking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple\nCollecting convert2php\n  Downloading https://files.pythonhosted.org/packages/30/ba/afea59e34b8493c3318251a299c4fbb4aa132981b1f1a29478a9b5f69183/convert2php-0.0.1.tar.gz\n    Complete output from command python setup.py egg_info:\n    Traceback (most recent call last):\n      File \"<string>\", line 1, in <module>\n      File \"/tmp/pip-install-AFkqGP/convert2php/setup.py\", line 9, in <module>\n        with open(os.path.join(os.path.dirname(__file__), 'README.md')) as readme:\n    IOError: [Errno 2] No such file or directory: '/tmp/pip-install-AFkqGP/convert2php/README.md'\n\n    ----------------------------------------\nCommand \"python setup.py egg_info\" failed with error code 1 in /tmp/pip-install-AFkqGP/convert2php/\n```\n\nUbuntu 20.04.2, pip 20.3.4(python 2.7) でも同じでした。\n\n```sh\n# curl https://bootstrap.pypa.io/pip/2.7/get-pip.py --output get-pip.py\n# python2 get-pip.py\n# pip install convert2php\nDEPRECATION: Python 2.7 reached the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 is no longer maintained. pip 21.0 will drop support for Python 2.7 in January 2021. More details about Python 2 support in pip can be found at https://pip.pypa.io/en/latest/development/release-process/#python-2-support pip 21.0 will remove support for this functionality.\nCollecting convert2php\n  Using cached convert2php-0.0.1.tar.gz (3.8 kB)\n    ERROR: Command errored out with exit status 1:\n     command: /usr/bin/python2 -c 'import sys, setuptools, tokenize; sys.argv[0] = '\"'\"'/tmp/pip-install-ro3dH9/convert2php/setup.py'\"'\"'; __file__='\"'\"'/tmp/pip-install-ro3dH9/convert2php/setup.py'\"'\"';f=getattr(tokenize, '\"'\"'open'\"'\"', open)(__file__);code=f.read().replace('\"'\"'\\r\\n'\"'\"', '\"'\"'\\n'\"'\"');f.close();exec(compile(code, __file__, '\"'\"'exec'\"'\"'))' egg_info --egg-base /tmp/pip-pip-egg-info-Uyfch2\n         cwd: /tmp/pip-install-ro3dH9/convert2php/\n    Complete output (5 lines):\n    Traceback (most recent call last):\n      File \"<string>\", line 1, in <module>\n      File \"/tmp/pip-install-ro3dH9/convert2php/setup.py\", line 9, in <module>\n        with open(os.path.join(os.path.dirname(__file__), 'README.md')) as readme:\n    IOError: [Errno 2] No such file or directory: '/tmp/pip-install-ro3dH9/convert2php/README.md'\n    ----------------------------------------\nERROR: Command errored out with exit status 1: python setup.py egg_info Check the logs for full command output.\n```\n\n<a href=\"https://github.com/danleyb2/php2python/issues/2\" target=\"_blank\">danleyb2/php2python/issues</a>に情報がありましたが、自動でダウンロードしている`convert2php-0.0.1.tar.gz`内にREADME.mdが存在しないのが原因のようです。  \n以下が正解になります。\n\n```sh\n# pip install https://github.com/danleyb2/php2python/archive/master.zip\nLooking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple\nCollecting https://github.com/danleyb2/php2python/archive/master.zip\n  Downloading https://github.com/danleyb2/php2python/archive/master.zip\n     \\ 20kB 5.4MB/s\nBuilding wheels for collected packages: convert2php\n  Running setup.py bdist_wheel for convert2php ... done\n  Stored in directory: /tmp/pip-ephem-wheel-cache-VnJSCL/wheels/ec/a1/26/3518603e2419732350d90abad491b052d62a58a7b5a3266859\nSuccessfully built convert2php\nInstalling collected packages: convert2php\nSuccessfully installed convert2php-0.0.1\n```\n\n<br />\n\n## 動作確認結果\nサンプルプログラム  \nsample.php  \nがあるとします。  \n\n```sh\n# convert2php -s sample.php\nINFO - Converting: sample.php. Output file will be: sample.py\nINFO - # Remove opening and closing <?php\nINFO - # convert $this-> to self.\nINFO - # convert :: to .\nINFO - # Process if statements\nINFO - # Process else statements\nINFO - # Process try statements\nINFO - # Process catch statements\nINFO - # delete all }\nINFO - # delete namespace|require_once|include_once\nINFO - # convert protected $var to self.var = None then move into __init__\nINFO - # convert public|protected function to def\nINFO - # add `self` to function signatures\nINFO - # classes not children to extend `object`\nINFO - # process child classes\nINFO - # convert $ to ''\nINFO - # convert ; to ''\nINFO - # convert new to ''\nINFO - # process foreach\nINFO - Converted: sample.php. to: sample.py. { Go on, Proof Check :) }\n```\n\nエラー無く、変換されました。変換結果を見てみます。\n\n```python\n\necho \"hello\\n\"\ndir = \"/path/to\"\nif( !preg_match( \"/\\//\", dir ) ) dir .= \"/\"\necho dir . \"\\n\"\n\na[] = \"1\"\na[] = \"2\"\na[] = \"3\"\nx = array()\nfor b  in  a :\n        x[] = b . \"b\"\n\nprint_r( x )\n\ni = 1\nif i == 0:\n    echo \"i=0\"\n    echo \"i=1\"\n    echo \"i=2\"\n\necho \"\\n\"\n\nswitch (i) {\n    case 0:\n        echo \"i=0\"\n        break\n    case 1:\n        echo \"i=1\"\n        break\n    case 2:\n        echo \"i=2\"\n        break\n\necho \"\\n\"\n\nclass User(object):\n  private nickname\n  def __init__(self,name,nickname):\n    self.name = None\n    self.name = name\n    self.nickname = nickname\n\nuser = User('foo','bar')\necho user->name\n```\n\n文法は置き換わっていますが、関数はそのままでした。  \n他、文字列を繋げるドットがそのままだったり、switch文がそのままだったりします。基本的なところしか変換しないようです。  \n<br />\n当然エラーになり、実行はできませんでした。\n\n```sh\n# python sample.py\n python sample.py\n  File \"sample.py\", line 2\n    echo \"hello\\n\"\n                 ^\nSyntaxError: invalid syntax\n```\n\n<br />\n\n<a class=\"anchor\" id=\"anchor5\"></a>\n\n# taichino/php2py\n<span style=\"color: red;\">CentOS</span>に <a href=\"https://github.com/taichino/php2py\" target=\"_blank\">taichino/php2py</a> をインストールして使ってみます。\n\n<blockquote class=\"warn\">\n<p>検証環境は、</p>\n<p><code>CentOS 7.6.1810</code></p>\n<p><code>boost_1_55_0</code></p>\n<p><code>PHP 5.4.45</code></p>\n<p>になります。</p>\n<p></blockquote>\n\n## 検証環境について\n\n`Raspbian GNU/Linux 10 (buster)`、`Ubuntu 20.04.2` にてインストールを実施しましたが、`phc (php compiler)`のビルドで以下のエラーになり、やや古い環境のCentOS 7.6でやりなおしたら、最後まで行きました。\n\n<strong>・Raspbian GNU/Linux 10 (buster)の場合</strong>\n\n```sh\n# cd /home/pi/phc\n# ./configure\nconfigure: error: cannot find Boost headers version >= 1.55.0\n# apt install -y libboost-all-dev\n# ./configure\nchecking for gc/gc_cpp.h... no\nconfigure: error: in `/home/pi/phc':\nconfigure: error: libgc required\nSee `config.log' for more details\n# apt install -y libgc-dev\n# ./configure\n# make\n./src/lib/Map.h:11:10: fatal error: boost/tr1/unordered_map.hpp: そのようなファイルやディレクトリはありません\n #include <boost/tr1/unordered_map.hpp>\n          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~\ncompilation terminated.\nmake[2]: *** [Makefile:2734: src/ast_to_hir/AST_annotate.lo] エラー 1\nmake[2]: ディレクトリ '/home/pi/phc' から出ます\nmake[1]: *** [Makefile:3094: all-recursive] エラー 1\nmake[1]: ディレクトリ '/home/pi/phc' から出ます\nmake: *** [Makefile:1607: all] エラー 2\n```\n\n<br />\n\n<strong>・Ubuntu 20.04.2の場合</strong>\n\n```sh\n# cd phc\n# touch src/generated/* Makefile.in configure Makefile libltdl/aclocal.m4 libltdl/Makefile.in libltdl/configure libltdl/Makefile\n# ./configure\nconfigure: error: C compiler cannot create executable\n```\n⇒Boostが無い以前の問題。めんどくさそうな気がして中止。\n\n<br />\n\n## インストール\nGitHubから php2py-master.zip をダウンロードします。その他、<a href=\"https://museum.php.net/php5/php-5.4.45.tar.gz\" target=\"_blank\">php-5.4.45.tar.gz</a>、<a href=\"https://sourceforge.net/projects/boost/files/boost/1.55.0/boost_1_55_0.tar.gz/download\" target=\"_blank\">boost_1_55_0.tar.gz</a>を配置します。  \n※ここでは、/home/admin に置くものとします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/php2python/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/php2python/image3.png\"  width=\"600px\" alt=\"\" loading=\"lazy\"></a>  \n\n<blockquote class=\"alert\">\n<p><span style=\"color: red\">READMEにある<code>http://www.phpcompiler.org/</code>へアクセスしないでください。無くなっていて、変なサイトに繋がります。</span></p>\n<p>phcは、GitHubの<a href=\"https://github.com/pbiggar/phc\">pbiggar/phc</a>に有りますが、ウィルスが検出されて、ダウンロードできませんでした。(手順中では<code>git clone</code>で取得しています。)</p>\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/php2python/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/php2python/image4.png\"  width=\"300px\" alt=\"\" loading=\"lazy\"></a>\n</blockquote>\n\n<br />\n\n### phpインストール\nphpをインストールします。\n\n```sh\n# tar zxf php-5.4.45.tar.gz\n# cd php-5.4.45\n# yum install libxml2-devel\n# ./configure --enable-embed\n# make && make install\n```\n\n<blockquote class=\"info\">\n<p>php-5.4.45にしたのは、taichino/php2pyのリリース日時からphp5系だろうと推測しただけでどのバージョンが正解なのかは良く分かりません。また、phcのconfigure時、以下のように--enable-embedを付けてインストールするように促されるため、ソースコードをビルドすることにしました。</p>\n<pre><code>configure: WARNING:\n*******************************************************************************\n* It seems the PHP embed SAPI has not been installed.                         *\n*                                                                             *\n* You will be able to compile and run phc, but you will not be able to        *\n* compile PHP scripts with phc.                                               *\n*                                                                             *\n* To install the PHP embed SAPI, follow the PHP installation instructions,    *\n* but make sure to pass the --enable-embed option to the PHP configure        *\n* script.                                                                     *\n*******************************************************************************</code></pre>\n</blockquote>\n\n<br />\n\n### Boostインストール\n\nBoostをインストールします。\n\n<blockquote class=\"info\">\n<p><strong>【 Boost 】</strong></p>\n<p>Boostとは、C++のライブラリ集になります。phcのビルドに必要です。</p>\n</blockquote>\n\n```sh\n# cd /home/admin\n# tar zxf boost_1_55_0.tar.gz\n# cd boost_1_55_0\n# ./bootstrap.sh --prefix=/usr/\n# ./b2 install\n```\n\n<blockquote class=\"warn\">\n<p>phcのビルドで <code>/usr/include/boost/tr1</code> が必要なようで、最新の<code>boost_1_76_0</code>ではインストールされませんでした。また、<code>boost_1_55_0</code>より古い場合は、phcのconfigureでエラーになります。</p>\n</blockquote>\n\n<br />\n\n### phcインストール\n\n`phc (php compiler)`をビルドします。いろいろエラーになりましたので、省略せず、直しながらの手順になります。\n\n```sh\n# cd /home/admin/\n# git clone https://github.com/pbiggar/phc.git\n# cd phc\n# touch src/generated/* Makefile.in configure Makefile libltdl/aclocal.m4 libltdl/Makefile.in libltdl/configure libltdl/Makefile\n# ./configure\nconfigure: error: cannot find the flags to link with Boost regex\n# vi m4/php-embed.m4\n```\n\n```sh\nLIBS=\"-lphp5 -L${PHP_INSTALL_PATH}/lib -R${PHP_INSTALL_PATH}/lib $LIBS\"\n↓\nLIBS=\"-lphp5 -L${PHP_INSTALL_PATH}/lib ${wl}-R${PHP_INSTALL_PATH}/lib $LIBS\"\n```\n\n```sh\n# ./autogen.sh\nconfigure.ac:20: error: require Automake 1.15, but have 1.13.4\n```\n⇒configureの作り直しには成功したので、無視。\n\n```sh\n# ./configure\nchecking for gc/gc_cpp.h... no\nconfigure: error: in `/home/admin/phc':\nconfigure: error: libgc required\n```\n⇒--disable-gcを付けてconfigureやり直し。\n\n```sh\n# ./configure --disable-gc\n# make\nsrc/embed/optimize.cpp: 静的メンバ関数 ‘static Method_info* PHP::get_method_info(String*)’ 内:\nsrc/embed/optimize.cpp:223:63: エラー: ‘zend_fcall_info* {aka _zend_fcall_info*}’ から ‘uint {aka unsigned int}’ への無効な変換です [-fpermissive]\n  int result = zend_fcall_info_init (&fn, &fci, &fcic TSRMLS_CC);\n                                                               ^\nsrc/embed/optimize.cpp:223:63: エラー: cannot convert ‘zend_fcall_info_cache* {aka _zend_fcall_info_cache*}’ to ‘zend_fcall_info* {aka _zend_fcall_info*}’ for argument ‘3’ to ‘int zend_fcall_info_init(zval*, uint, zend_fcall_info*, zend_fcall_info_cache*, char**, char**)’\nsrc/embed/optimize.cpp: メンバ関数 ‘virtual bool Internal_method_info::return_by_ref()’ 内:\nsrc/embed/optimize.cpp:248:22: エラー: ‘struct _zend_function::<anonymous>’ has no member named ‘return_reference’\n  return func->common.return_reference;\n                      ^\nsrc/embed/optimize.cpp:249:1: 警告: 制御が非 void 関数の終りに到達しました [-Wreturn-type]\n }\n ^\n大域スコープ:\ncc1plus: 警告: 認識できないコマンドラインオプション \"-Wno-unused-local-typedef\" です [デフォルトで有効]\nmake[2]: *** [src/embed/optimize.lo] エラー 1\nmake[2]: ディレクトリ `/home/admin/phc' から出ます\nmake[1]: *** [all-recursive] エラー 1\nmake[1]: ディレクトリ `/home/admin/phc' から出ます\nmake: *** [all] エラー 2\n```\n\n```sh\n# vi src/embed/optimize.cpp\n```\n\n```sh\n223行目\nint result = zend_fcall_info_init (&fn, &fci, &fcic TSRMLS_CC);\n↓\nint result = zend_fcall_info_init (&fn, 0, &fci, &fcic, NULL, NULL TSRMLS_CC);\n```\n\n```sh\n# make\nsrc/embed/optimize.cpp: メンバ関数 ‘virtual bool Internal_method_info::return_by_ref()’ 内:\nsrc/embed/optimize.cpp:248:22: エラー: ‘struct _zend_function::<anonymous>’ has no member named ‘return_reference’\n  return func->common.return_reference;\n                      ^\nsrc/embed/optimize.cpp:249:1: 警告: 制御が非 void 関数の終りに到達しました [-Wreturn-type]\n }\n ^\n大域スコープ:\ncc1plus: 警告: 認識できないコマンドラインオプション \"-Wno-unused-local-typedef\" です [デフォルトで有効]\nmake[2]: *** [src/embed/optimize.lo] エラー 1\nmake[2]: ディレクトリ `/home/admin/phc' から出ます\nmake[1]: *** [all-recursive] エラー 1\nmake[1]: ディレクトリ `/home/admin/phc' から出ます\nmake: *** [all] エラー 2\n```\n\n```sh\n# vi src/embed/optimize.cpp\n```\n\n```sh\n248行目\nreturn func->common.return_reference;\n↓\n#ifdef ZEND_ENGINE_2\n\treturn (func->common.fn_flags & ZEND_ACC_RETURN_REFERENCE);\n#else\n\treturn func->common.return_reference;\n#endif\n```\n\n```sh\n# make\n# make install\n```\n\nこれでインストール完了ですが、まだ問題があり、phcコマンドを起動するとエラーになります。\n\n```sh\n# phc\nphc: error while loading shared libraries: libboost_regex.so.1.55.0: cannot open shared object file: No such file or directory\n```\n\nシンボリックリンクを作成します。\n```sh\n# ln -s /usr/lib/libboost_regex.so.1.55.0 /usr/local/lib/libboost_regex.so.1.55.0\n```\n\nようやく問題がなくなりました。\n\n<br />\n\n## 動作確認結果\nサンプルプログラム  \n/home/admin/php2py-master.zip  \n/home/admin/sample.php  \nがあるとします。  \n\n```sh\n# cd /home/admin\n# unzip php2py-master.zip\n# cd php2py-master\n# cp ../sample.php .\n# phc --dump-xml=ast sample.php > sample.xml\n# chmod 755 ./php2py.php\n# vi php2py.php\n```\n\n```sh\n#!/usr/bin/php\n↓\n#!/usr/local/bin/php\n```\n\n```sh\n# ./php2py.php sample.xml > sample.py\n```\n\nエラー無く、変換されました。変換結果を見てみます。\n\n```python\n#!/usr/bin/python\n#-*- coding: utf-8 -*-\nprint(\"hello\\n\")\ndir = \"/path/to\"\nif !preg_match(\"/\\/$/\", dir):\n  dir+=\"/\"\nprint(dir+\"\\n\")\na[] = \"1\"\na[] = \"2\"\na[] = \"3\"\nx = []\nfor b in a:\n  x[] = b+\"b\"\nprint_r(x)\ni = 1\nif i==0:\n  print(\"i=0\")\nelif i==1:\n  print(\"i=1\")\nelif i==2:\n  print(\"i=2\")\nprint(\"\\n\")\nif i==0:\n  print(\"i=0\")\nelif i==1:\n  print(\"i=1\")\nelif i==2:\n  print(\"i=2\")\nprint(\"\\n\")\n\nclass User:\n  name=0\n  nickname=0\n  def __init__(name, nickname):\n    self.name = name\n    self.nickname = nickname\n\nuser = User(\"foo\", \"bar\")\nprint(user.name)\n```\n\n文法が書き換わり、`echo`が`print`に変わりました。ただ、`preg_match`、`print_r`関数はそのままでした。  \nまた、配列の`a[] = \"1\"`のところはそのままでは動作しません。\n他、文字列を繋げるドットがプラス記号に変わっていたり、switch文がif文に変わっていたりするので、先ほどの、`danleyb2/php2python`よりは良いような気がします。  \n<br />\n当然エラーになり、実行はできませんでした。\n\n```sh\n# python sample.py\n  File \"sample.py\", line 5\n    if !preg_match(\"/\\/$/\", dir):\n       ^\nSyntaxError: invalid syntax\n```\n\n<br />\n\n<a class=\"anchor\" id=\"anchor6\"></a>\n\n# おまけ php→js\n\nNode.jsのnpmで babel-preset-php というphp→jsのトランスパイラがあります。pythonに変換では無いですが、こちらは、どうなのか、やってみました。\n\n<blockquote class=\"warn\">\n<p>検証環境は、</p>\n<p><code>Raspbian GNU/Linux 10 (buster)</code></p>\n<p><code>node v10.24.0</code></p>\n<p><code>npm 5.8.0</code></p>\n<p><code>6.26.0 (babel-core 6.26.3)</code></p>\n<p>になります。</p>\n<p></blockquote>\n\n```sh\n# apt update\n# apt install nodejs\n# apt install npm\n# mkdir sample\n# cd sample\n# npm init -y\n# npm i -S babel-preset-php\n# vi .babelrc\n{\n  \"presets\": [\"php\"]\n}\n# npm i -g babel-cli\n# cp ../sample.php .\n# babel sample.php -o sample.js\nError: Plugin 0 specified in \"/home/pi/sample/node_modules/babel-preset-php/src/index.js\" provided an invalid property of \"default\" (While processing preset: \"/home/pi/sample/node_modules/babel-preset-php/src/index.js\")\n    at Plugin.init (/usr/local/lib/node_modules/babel-cli/node_modules/babel-core/lib/transformation/plugin.js:131:13)\n    at Function.normalisePlugin (/usr/local/lib/node_modules/babel-cli/node_modules/babel-core/lib/transformation/file/options/option-manager.js:152:12)\n    at /usr/local/lib/node_modules/babel-cli/node_modules/babel-core/lib/transformation/file/options/option-manager.js:184:30\n    at Array.map (<anonymous>)\n    at Function.normalisePlugins (/usr/local/lib/node_modules/babel-cli/node_modules/babel-core/lib/transformation/file/options/option-manager.js:158:20)\n    at OptionManager.mergeOptions (/usr/local/lib/node_modules/babel-cli/node_modules/babel-core/lib/transformation/file/options/option-manager.js:234:36)\n    at /usr/local/lib/node_modules/babel-cli/node_modules/babel-core/lib/transformation/file/options/option-manager.js:265:14\n    at /usr/local/lib/node_modules/babel-cli/node_modules/babel-core/lib/transformation/file/options/option-manager.js:323:22\n    at Array.map (<anonymous>)\n    at OptionManager.resolvePresets (/usr/local/lib/node_modules/babel-cli/node_modules/babel-core/lib/transformation/file/options/option-manager.js:275:20)\n```\n\nエラーになりました。\nbabel6系の場合、babel-preset-php 1.* 系を使わないといけないようです。(<a href=\"https://stackoverflow.com/questions/67812727/error-plugin-0-specified-in-my-dir-node-modules-babel-preset-php-src-index-js\" target=\"_blank\">参考</a>)\n\n```sh\n# vi package.json\n\"babel-preset-php\": \"^2.0.0\",\n↓\n\"babel-preset-php\": \"^1.0.0\",\n\n# rm -rf node_modules package-lock.json\n# npm install\n# babel sample.php -o sample.js\n```\n\n変換できましたので、sample.jsを見てみます。\n\n```javascript\necho(\"hello\\n\");\nvar dir = \"/path/to\";\nif (!preg_match(\"/\\\\/$/\", dir)) dir += \"/\";\necho(dir + \"\\n\");\na.push(\"1\");\na.push(\"2\");\na.push(\"3\");\nvar x = Array();\n\nfor (var b of Object.values(a)) {\n    x.push(b + \"b\");\n}\n\nconsole.log(x);\nvar i = 1;\n\nif (i == 0) {\n    echo(\"i=0\");\n} else if (i == 1) {\n    echo(\"i=1\");\n} else if (i == 2) {\n    echo(\"i=2\");\n}\n\necho(\"\\n\");\n\nswitch (i) {\n    case 0:\n        echo(\"i=0\");\n        break;\n\n    case 1:\n        echo(\"i=1\");\n        break;\n\n    case 2:\n        echo(\"i=2\");\n        break;\n}\n\necho(\"\\n\");\n\nclass User {\n    constructor(name, nickname) {\n        this.name = name;\n        this.nickname = nickname;\n    }\n\n};\n\nvar user = new User(\"foo\", \"bar\");\necho(user.name);\n```\n\n文法は置き換わりましたが、`echo`が`echo`のままで、`preg_match`がそのままです。関数の置き換えまでは、各言語で微妙な挙動の違いもありますし、厳しいと思われます。  \n実行すると、以下のエラーになりました。  \n↓  \n```sh\n# node sample.js\n/home/pi/sample/sample.js:1\necho(\"hello\\n\");\n^\n\nReferenceError: echo is not defined\n    at Object.<anonymous> (/home/pi/sample/sample.js:1:1)\n    at Module._compile (internal/modules/cjs/loader.js:778:30)\n    at Object.Module._extensions..js (internal/modules/cjs/loader.js:789:10)\n    at Module.load (internal/modules/cjs/loader.js:653:32)\n    at tryModuleLoad (internal/modules/cjs/loader.js:593:12)\n    at Function.Module._load (internal/modules/cjs/loader.js:585:3)\n    at Function.Module.runMain (internal/modules/cjs/loader.js:831:12)\n    at startup (internal/bootstrap/node.js:283:19)\n    at bootstrapNodeJSCore (internal/bootstrap/node.js:623:3)\n```\n\n\n","description":"ラズパイでBME280センサから取得した温湿度気圧をWeb画面に表示する wiringpi-php-bme280(GitHub) というのを作りました。 こちらは、phpのプログラムですが、同じことがpythonでもできるはずです。 そこで、まず、ツールでphp→pythonの変換をして、それを元に開発することにしました。今回は、変換ツール(トランスパイラ)の選定と試した結果を書きたいと思います。  【 トランスパイル(transpile) 】  トランスパイル(transpile)とは、あるプログラミング言語から他のプログラミング言語へと変換(翻訳)することです。変換ツールをトランスパイラと呼びます。JavaScriptのBabelが有名です。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"53ux8y5zb9","createdAt":"2021-07-20T13:21:17.798Z","updatedAt":"2021-07-20T13:21:17.798Z","publishedAt":"2021-07-20T13:21:17.798Z","revisedAt":"2021-07-20T13:21:17.798Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/php2python/ITC_Engineering_Blog.png","alt":"php→pythonのトランスパイル","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 を構成する1つで 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 = 先頭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(先頭7文字)>\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// 注意:今回は、1回しか使っていない。\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# 動作確認1\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# 動作確認2\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":"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 = 先頭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(先頭7文字)>\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(先頭7文字)>\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// 注意:今回は、1回しか使っていない。\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 のインスタンス のリソースIDを格納します。\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# 動作確認1\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# 動作確認2\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 に復帰する時、1分近く待ちますので、注意が必要です。  \n今回の場合、(Pod レプリカ数 0 の状態で)ブラウザでアクセスすると、読み込み待ちが1分近く続きました。  \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":"oauth2-proxy-azuread","createdAt":"2024-01-06T09:36:06.185Z","updatedAt":"2024-03-02T10:16:43.365Z","publishedAt":"2024-01-06T09:36:06.185Z","revisedAt":"2024-03-02T10:16:43.365Z","title":"oauth2-proxyで既存アプリにAzure AD OpenID Connect認証機能を追加","category":{"id":"9wvhex1nhpyg","createdAt":"2023-12-29T12:09:49.231Z","updatedAt":"2023-12-29T12:09:49.231Z","publishedAt":"2023-12-29T12:09:49.231Z","revisedAt":"2023-12-29T12:09:49.231Z","topics":"Go","logo":"/logos/Go.png","needs_title":false},"topics":[{"id":"9wvhex1nhpyg","createdAt":"2023-12-29T12:09:49.231Z","updatedAt":"2023-12-29T12:09:49.231Z","publishedAt":"2023-12-29T12:09:49.231Z","revisedAt":"2023-12-29T12:09:49.231Z","topics":"Go","logo":"/logos/Go.png","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":"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":"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}],"content":"# はじめに\n\noauth2-proxy で既存アプリに Azure AD(Microsoft Entra ID) OpenID Connect 認証機能を追加しました。\n\n<br />\n\n過去記事で、SimpleSAMLphp, go-oidc と同じようなことをやってきましたが、今回の構成は、プロキシ型の認証になります。  \nプロキシ型の特徴として、<span style=\"color: red;\"><strong>既存の Web アプリケーションの実装を変更しないまま認証機能を実現できる</strong></span>ということがあります。  \nプロキシ型の認証は、アプリケーションに認証ロジックを組み込むことが困難な場合や、認証機能を分離したい場合に有用です。\n\n<br />\n\n構成は、<a href=\"https://oauth2-proxy.github.io/oauth2-proxy/\" target=\"_blank\">公式サイト</a>(`https://oauth2-proxy.github.io/oauth2-proxy/`)Architecture の図の左側です。\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image1.png\" alt=\"Architecture\" width=\"392\" height=\"372\" loading=\"lazy\"></a>\n\n<br />\n\n今回、簡単なアプリケーション実装(PHP で `phpinfo()` を実行するだけ。) ~  \nAzure AD(Microsoft Entra ID)設定 ~  \noauth2-proxy 設定 ~  \nOpenID Connect 認証を通過したらアプリにアクセス  \nまで全手順を紹介していきます。  \nなお、<span style=\"color: red;\"><strong>oauth2-proxy は、VSCode で F5 で起動してデバッグできるようにします。</strong></span>\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=\"warn\">\n<p>手順は、OS(Ubuntu22) インストール & SSH 接続成功直後からスタートとします。</p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>今回の手順では、Nginx & oauth2-proxy と Nginx & Web アプリケーション(PHP) は同一サーバーに同居します。</p>\n<p><a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image2.png\" alt=\"同一サーバーに同居 図\" width=\"852\" height=\"432\" loading=\"lazy\"></a></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.1</p>\n<p>oauth2-proxy V7.5.1</p>\n</blockquote>\n\n<br />\n\n# Web アプリケーション環境構築\n\n<blockquote class=\"info\">\n<p>今回、Web アプリケーションサーバーの URI は、</p>\n<p><code>http://webapps-php.example.com/info.php</code> とします。</p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>root で作業するため、sudo は省略しています。</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.1 (cli) (built: Dec 21 2023 20:12:13) (NTS)\n# apt -y remove apache2-*\n# apt install php-fpm -y\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# vi /etc/nginx/conf.d/webapps-info.conf\n```\n\n```nginx:/etc/nginx/conf.d/webapps-info.conf\nserver  # サーバーブロックの開始\n{\n  listen 80;  # サーバーが待ち受けるポート番号(HTTPのデフォルトポートは80)\n\n  server_name webapps-php.example.com;  # サーバーの名前(ドメイン名)\n\n  access_log /var/log/webapps/php/access.log;  # アクセスログの出力先\n  error_log /var/log/webapps/php/error.log;  # エラーログの出力先\n\n  proxy_buffer_size 8k;  # プロキシバッファのサイズ(8キロバイト)\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\n    if (!-f $document_root$fastcgi_script_name)  # ファイルが存在しない場合\n    {\n      return 404;  # 404エラーを返す\n    }\n\n    client_max_body_size 100m;  # クライアントからの最大ボディサイズ(100メガバイト)\n\n    fastcgi_param HTTP_PROXY \"\";  # HTTP_PROXYパラメータを空に設定(セキュリティ対策)\n\n    fastcgi_pass unix:/run/php/php8.3-fpm.sock;  # FastCGIサーバーへのパス\n    fastcgi_index index.php;  # FastCGIサーバーのインデックスファイル\n\n    include fastcgi_params;  # FastCGIパラメータのインクルード\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\n```shellsession\n# vi /etc/hosts\n192.168.11.9 webapps-php.example.com\n```\n\n<blockquote class=\"info\">\n<p>今回、Web アプリケーションサーバーの IP アドレスは、192.168.11.9 とします。</p>\n</blockquote>\n\n```shellsession\n# systemctl restart nginx\n# systemctl restart php8.3-fpm\n```\n\n<br />\n\nひとまず、`http://webapps-php.example.com/info.php` にアクセスして、単体で動作しているか確認します。\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image3.png\" alt=\"単体で動作しているか確認\" width=\"1077\" height=\"342\" loading=\"lazy\"></a>\n\n<blockquote class=\"alert\">\n<p>本来であれば、直接アクセス不可の場所に構築されているはずですが、ここでは簡略化のため、このまま進めます。(この記事の最後まで実施しても :80 直接アクセスの場合、認証無しになります。)</p>\n</blockquote>\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image4.png\" alt=\"単体で動作しているか確認 図\" width=\"851\" height=\"431\" loading=\"lazy\"></a>\n\n<br />\n\n# golang-go インストール\n\nGo 言語をインストールします。\n\n```shellsession\n# apt update -y\n# apt install golang-go -y\n# go version\ngo version go1.18.1 linux/amd64\n```\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image5.png\" alt=\"Go 言語をインストール 図\" width=\"851\" height=\"431\" loading=\"lazy\"></a>\n\n<br />\n\n# oauth2-proxy 設置\n\n`/opt/oauth2-proxy` を配置します。\n\n```shellsession\n# apt install git -y\n# cd /opt\n# git clone https://github.com/oauth2-proxy/oauth2-proxy.git\n```\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image6.png\" alt=\"oauth2-proxy 設置 図\" width=\"851\" height=\"431\" loading=\"lazy\"></a>\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**名前**: `oauth2-proxy`(任意です。)  \n**サポートされているアカウントの種類**:`この組織ディレクトリのみに含まれるアカウント (<テナント名> のみ - シングル テナント)`  \n**リダイレクト URI (省略可能)**:`Web` `https://webapps-php.example.com/oauth2/callback`\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image7.png\" alt=\"アプリ情報を入力\" width=\"1091\" height=\"818\" loading=\"lazy\"></a>\n\n<br />\n\n**概要** をクリックして、  \n**アプリケーション (クライアント) ID** から client_id(後で行う oauth2-proxy 設定項目)を確認しておきます。\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image8.png\" alt=\"アプリケーション (クライアント) ID\" width=\"1089\" height=\"697\" loading=\"lazy\"></a>\n\n<br />\n\n**証明書とシークレット** をクリックし、**+新しいクライアント シークレット** をクリックします。\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image9.png\" alt=\"新しいクライアント シークレット\" width=\"1090\" height=\"566\" loading=\"lazy\"></a>\n\n<br />\n\n**説明**、**有効期限** を任意の値に設定し、**追加** をクリックします。\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image10.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image10.png\" alt=\"クライアント シークレット追加\" width=\"1090\" height=\"504\" loading=\"lazy\"></a>\n\n<br />\n\n<span style=\"color: red;\"><strong>ここで出てくる **値** の文字列が client_secret(後で行う oauth2-proxy 設定項目)の文字列になります。(シークレット ID の方ではありません。)</strong></span>  \n<span style=\"color: red;\"><strong>二度と表示されないため、ここで、メモっておきます。</strong></span>\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image11.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image11.png\" alt=\"クライアント シークレットの値\" width=\"1090\" height=\"627\" loading=\"lazy\"></a>\n\n<br />\n\nグループ読み取りアクセス許可を追加します。\n\n<blockquote class=\"info\">\n<p><a href=\"https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider/#azure-auth-provider\" target=\"_blank\">公式ドキュメント</a>(<code>https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider/#azure-auth-provider</code>)に指示がある手順です。  </p>\n<p>必須ではないかもしれません。今回の手順では、これを省略しても動作しました。</p>\n</blockquote>\n\n**API アクセス許可** をクリックし、**Microsoft Graph** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image12.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image12.png\" alt=\"API アクセス許可 Microsoft Graph\" width=\"1149\" height=\"570\" loading=\"lazy\"></a>\n\n<br />\n\n**Group.Read.All** にチェックを入れて、**アクセス許可の更新** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image13.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image13.png\" alt=\"Group.Read.Allにチェック アクセス許可の更新\" width=\"1149\" height=\"865\" loading=\"lazy\"></a>\n\n<br />\n\n*****に管理者の同意を与えます** をクリックして、**はい** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image14.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image14.png\" alt=\"***に管理者の同意を与えます\" width=\"1148\" height=\"666\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image15.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image15.png\" alt=\"***に管理者の同意を与えます はい\" width=\"1136\" height=\"409\" loading=\"lazy\"></a>\n\n<blockquote class=\"info\">\n<p><a href=\"https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider/#azure-auth-provider\" target=\"_blank\">公式ドキュメント</a>(<code>https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider/#azure-auth-provider</code>)に指示がある手順です。  </p>\n<p>これを行わないと以下のエラーになります。  </p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">管理者の承認が必要</span>  </p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">には、組織内のリソースへのアクセス許可が必要です。このアクセス許可を付与できるのは管理者のみです。アプリケーションを使用するには、まず管理者に依頼してこのアプリにアクセス許可を付与してください。</span></p>\n<p><a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image16.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image16.png\" alt=\"管理者の承認が必要\" width=\"773\" height=\"551\" loading=\"lazy\"></a></p>\n</blockquote>\n\n<br />\n\nv2.0 Azure Auth エンドポイントを使用する予定がある場合は、**マニフェスト** をクリックして、マニフェストページに移動し、  \n`\"accessTokenAcceptedVersion\": 2`  \nをアプリ登録マニフェストファイルに設定し、**保存** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image17.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image17.png\" alt=\"accessTokenAcceptedVersion=2\" width=\"1146\" height=\"666\" loading=\"lazy\"></a>\n\n<blockquote class=\"info\">\n<p><a href=\"https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider/#azure-auth-provider\" target=\"_blank\">公式ドキュメント</a>(<code>https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider/#azure-auth-provider</code>)に指示がある手順です。</p>\n<p>今回の手順では、必須ではありません。</p>\n</blockquote>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image18.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image18.png\" alt=\"Azure AD - アプリの登録 図\" width=\"851\" height=\"431\" loading=\"lazy\"></a>\n\n<br />\n\n# VSCode 拡張機能 Go インストール\n\nSSH Remote 機能により、oauth2-proxy の Ubuntu に接続し、/opt/oauth2-proxy を開きます。\n\n<blockquote class=\"info\">\n<p>SSH Remote 機能の手順に関しては、別記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/alpine-vscode-ssh\" target=\"_blank\">Alpine Linux をインストールして VS Code Remote SSH してみた</a>」を参考にしてください。</p>\n</blockquote>\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image19.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image19.png\" alt=\"/opt/oauth2-proxy を開く\" width=\"1059\" height=\"147\" loading=\"lazy\"></a>\n\n<br />\n\n拡張機能 Go をインストールします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image20.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image20.png\" alt=\"拡張機能 Go をインストール\" width=\"1023\" height=\"332\" loading=\"lazy\"></a>\n\n<br />\n\n拡張機能 Go が必要とする gopls をインストールします。\n\n```shellsession\n# go install -v golang.org/x/tools/gopls@latest\n```\n\n<blockquote class=\"info\">\n<p>gopls(Go Please/ゴプルス)は、Go 言語が公式にサポートしている Language Server です。これは、エディタや統合開発環境(IDE)がソースコードを解析し、コード補完やシンボルのリネームなどの機能を提供するためのツールです。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>インストールが必要とメッセージが表示される場合、Install をクリックしても同じことです。</p>\n<p><a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image21.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image21.png\" alt=\"gopls(Go Please/ゴプルス)Install をクリック\" width=\"1018\" height=\"772\" loading=\"lazy\"></a></p>\n</blockquote>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image22.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image22.png\" alt=\"VSCode 拡張機能 Go インストール 図\" width=\"851\" height=\"431\" loading=\"lazy\"></a>\n\n<br />\n\n# Nginx oauth2-proxy 用設定追加\n\nNginx から oauth2-proxy へ通信が行われるように設定します。\n\n```shellsession\n# mkdir -p /var/log/webapps/oauth2-proxy\n```\n\n<br />\n\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.11.9\" > 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/oauth2-proxy.crt\n# mv ca.key /etc/pki/tls/private/oauth2-proxy.key\n# mv ca.csr /etc/pki/tls/private/oauth2-proxy.csr\n```\n\n<br />\n\nNginx の設定を追加します。\n\n```shellsession\n# vi /etc/nginx/conf.d/oauth2-proxy.conf\n```\n\n```nginx:/etc/nginx/conf.d/oauth2-proxy.conf\nserver # サーバーブロックの開始。このブロック内の設定は、特定のサーバーまたは仮想ホストに適用されます。\n{\n  listen 443 ssl; # サーバーが443ポートでSSL接続をリッスンするように指示します。\n  ssl_certificate /etc/pki/tls/certs/oauth2-proxy.crt; # SSL証明書のパスを指定します。\n  ssl_certificate_key /etc/pki/tls/private/oauth2-proxy.key; # SSL証明書の秘密鍵のパスを指定します。\n  server_name _; # サーバー名を指定します。ここではワイルドカード(_)が使用されています。\n  access_log /var/log/webapps/oauth2-proxy/access.log; # アクセスログの出力先を指定します。\n  error_log /var/log/webapps/oauth2-proxy/error.log; # エラーログの出力先を指定します。\n\n  proxy_buffer_size 8k; # プロキシバッファのサイズを8KBに設定します。\n  resolver 127.0.0.53 ipv6=off; # DNSリゾルバとして127.0.0.53を使用し、IPv6を無効にします。\n\n  location / # ロケーションブロックの開始。このブロック内の設定は、特定のURLパターンに適用されます。\n  {\n    auth_request /oauth2/auth; # 認証リクエストを指定したURLに送信します。\n    error_page 401 = /oauth2/sign_in?rd=$request_uri; # 401エラー(未認証)が発生した場合のリダイレクト先を指定します。\n    index index.html index.htm index.php; # ディレクトリインデックスとして使用するファイルを指定します。\n    proxy_pass http://$host; # リクエストを指定したプロキシサーバーに転送します。\n    #proxy_ssl_verify off; # プロキシサーバーのSSL証明書の検証を無効にします(コメントアウトされています)。\n  }\n\n  location /oauth2/ { # /oauth2/ ロケーションブロックの開始。\n    proxy_pass http://127.0.0.1:4180; # リクエストを指定したoauth2-proxyサーバーに転送します。\n    proxy_set_header Host $host; # プロキシリクエストのHostヘッダーを設定します。\n    proxy_set_header X-Real-IP $remote_addr; # プロキシリクエストのX-Real-IPヘッダーを設定します。\n    proxy_set_header X-Scheme $scheme; # プロキシリクエストのX-Schemeヘッダーを設定します。\n  }\n\n  location /oauth2/auth { # /oauth2/auth ロケーションブロックの開始。\n    proxy_pass http://127.0.0.1:4180; # リクエストを指定したoauth2-proxyサーバーに転送します。\n    proxy_set_header Host $host; # プロキシリクエストのHostヘッダーを設定します。\n    proxy_set_header X-Real-IP $remote_addr; # プロキシリクエストのX-Real-IPヘッダーを設定します。\n    proxy_set_header X-Scheme $scheme; # プロキシリクエストのX-Schemeヘッダーを設定します。\n    proxy_set_header Content-Length \"\"; # プロキシリクエストのContent-Lengthヘッダーを空に設定します。\n    proxy_pass_request_body off; # プロキシリクエストのボディの転送を無効にします。\n  }\n}\n```\n\n<blockquote class=\"warn\">\n<p><code>resolver 127.0.0.53 ipv6=off;</code> の部分は、/etc/hosts を読み取る設定です。</p>\n<p>今回の場合、<code>webapps-php.example.com</code> の名前解決のために必要です。</p>\n</blockquote>\n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;\"><strong><code>error_page 401 = /oauth2/sign_in?rd=$request_uri;</code></strong></span></p>\n<p><span style=\"color: red;\"><strong>の<code>?rb=</code>の部分は、認証通過後、元々アクセスしに来た URI に戻るために必要です。</strong></span></p>\n<p>これが無いと、以下の例のように / に戻ります。</p>\n<p>例:</p>\n<p><code>https://webapps-php.example.com/info.php</code></p>\n<p>↓</p>\n<p>認証通過</p>\n<p>↓</p>\n<p><code>https://webapps-php.example.com</code></p>\n</blockquote>\n\n<br />\n\nNginx の設定を反映します。\n\n```shellsession\n# systemctl restart nginx\n```\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image23.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image23.png\" alt=\"Nginx oauth2-proxy 用設定追加 図\" width=\"851\" height=\"431\" loading=\"lazy\"></a>\n\n<br />\n\n# VSCode でデバッグ起動環境作成\n\n今回、oauth2-proxy (の main.go)を F5(デバッグモード) で起動します。\n起動するための準備になります。\n\nmain.go を開いて、\n**実行とデバッグ** → **launch.json ファイルを作成します** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image24.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image24.png\" alt=\"launch.json ファイルを作成します\" width=\"1021\" height=\"384\" loading=\"lazy\"></a>\n\n<br />\n\n**Go: Launch Package** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image25.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image25.png\" alt=\"Go: Launch Package\" width=\"1022\" height=\"135\" loading=\"lazy\"></a>\n\n<br />\n\nlaunch.json が作成されたら準備完了です。\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image26.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image26.png\" alt=\"launch.json\" width=\"1021\" height=\"398\" loading=\"lazy\"></a>\n\n<br />\n\nとりあえず、main.go に戻って、F5 キーを押して起動します。  \n`The \"dlv\" command is not available. Run \"go install -v github.com/go-delve/delve/cmd/dlv@latest\" to install.`  \nと表示されるため、  \n**Install** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image27.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image27.png\" alt=\"dlv Install\" width=\"1020\" height=\"768\" loading=\"lazy\"></a>\n\n<blockquote class=\"info\">\n<p>ここでインストールされる dlv(delve) は、Go 言語のデバッガです。単体でも機能しますが、VSCode と連携して VSCode で Go 言語のデバッグが可能になります。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p><code>go install -v github.com/go-delve/delve/cmd/dlv@latest</code> でインストールしても構いません。</p>\n</blockquote>\n\nインストールし終えたら、F5 で起動!  \nといきたいところですが、今回の環境の場合、以下のエラーになり、起動できません。  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Build Error: go build -o /opt/oauth2-proxy/\\_\\_debug_bin2712808634 -gcflags all=-N -l .</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\"># google.golang.org/grpc</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">/root/go/pkg/mod/google.golang.org/grpc@v1.58.3/server.go:2096:14: undefined: atomic.Int64</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">note: module requires Go 1.19 (exit status 2)</span>\n\nGo のバージョンが低すぎるため、ビルドエラーです。\n\n<br />\n\n# Go 更新\n\nGo を最新版に更新します。\n\n```shellsession\n# add-apt-repository ppa:longsleep/golang-backports -y\n# apt update\n# apt install golang-go -y\n# go version\ngo version go1.21.4 linux/amd64\n```\n\n<br />\n\nVSCode で  \n`Tools (gopls, dlv) need recompiling to work with go version go1.21.4 linux/amd64`  \nと表示されるため、  \n**Update tools** をクリックします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image28.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image28.png\" alt=\"Update tools\" width=\"1023\" height=\"768\" loading=\"lazy\"></a>\n\n<br />\n\n再び、F5 で main.go を起動します。  \nビルドエラーは解消して、起動しようとするところまで進みますが、設定を全く指定していないため、以下のエラーになります。  \n<span style=\"color: #e70500;background-color: #ffebe7;\">[2024/01/04 21:07:24] [main.go:54] invalid configuration:</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\"> missing setting: cookie-secret</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\"> provider missing setting: client-id</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\"> missing setting: client-secret or client-secret-file</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\"> missing setting for email validation: email-domain or authenticated-emails-file required.</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\"> use email-domain=\\* to authorize all email addresses</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Process 12927 has exited with status 1</span>  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Detaching</span>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image29.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image29.png\" alt=\"F5 で main.go を起動 図\" width=\"851\" height=\"431\" loading=\"lazy\"></a>\n\n<br />\n\n# oauth2-proxy 設定\n\noauth2-proxy の設定を行います。\n\n<br />\n\n起動オプションで設定もできますが、今回は、設定ファイルで設定します。\n\n<br />\n\nそのためには、まず、  \n`--config=/opt/oauth2-proxy/oauth2-proxy.cfg` オプションありで oauth-proxy を起動して、設定ファイル /opt/oauth2-proxy/oauth2-proxy.cfg を読み込むようにします。  \nオプションを付けてデバッグ起動したいので、launch.json に  \n`\"args\": [\"--config=/opt/oauth2-proxy/oauth2-proxy.cfg\"],`  \nを追加して、以下のようにします。\n\n```json:/opt/oauth2-proxy/.vscode/launch.json\n{\n    // IntelliSense を使用して利用可能な属性を学べます。\n    // 既存の属性の説明をホバーして表示します。\n    // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"name\": \"Launch Package\",\n            \"type\": \"go\",\n            \"request\": \"launch\",\n            \"mode\": \"auto\",\n            \"program\": \"${fileDirname}\",\n            \"args\": [\"--config=/opt/oauth2-proxy/oauth2-proxy.cfg\"],\n        }\n    ]\n}\n```\n\n<br />\n\noauth2-proxy.cfg を編集します。\n\n```shellsession\n# head -c 24 /dev/urandom | base64\n6s1wlAH00x80vPOftMiBwCzcEeyr23y4\n```\n\n```shellsession\n# vi /opt/oauth2-proxy/oauth2-proxy.cfg\n```\n\n```ini:/opt/oauth2-proxy/oauth2-proxy.cfg\n# Azure ADを使用して認証を行います。\nprovider = \"azure\"\n# OAuth2 ProxyがHTTP/HTTPSクライアントを待ち受けるアドレスとポートを指定します。\nhttp_address = \"127.0.0.1:4180\"\n# 許可されるEメールのドメインを指定します。ここではワイルドカードが使われており、どのドメインのEメールアドレスも許可されます。\nemail_domains = [\"*\"]\n# OAuth2 プロバイダに要求するスコープを指定します。\"openid\" は OpenID Connect フローを使用することを意味します。\nscope = \"openid\"\n# OAuth2プロキシが発行するCookieのシードに使われる値を指定します。\ncookie_secret = \"6s1wlAH00x80vPOftMiBwCzcEeyr23y4\"\n# この行はコメントアウトされていますが、もし有効化された場合、CookieがHTTPS接続でのみ送信されることを指定します。\n#cookie_secure = false\n# この行もコメントアウトされていますが、もし有効化された場合、セッションCookieが最小限の情報のみを含むようになります。\n#session_cookie_minimal = true\n# OpenID ConnectプロバイダのURLを指定します。\noidc_issuer_url = \"https://login.microsoftonline.com/<Azure AD ディレクトリ (テナント) ID>/v2.0\"\n# OAuth2プロバイダに登録したクライアントのIDを指定します。\nclient_id = \"<Azure AD アプリケーション (クライアント) ID>\"\n# OAuth2プロバイダに登録したクライアントシークレットを指定します。\nclient_secret = \"<Azure AD 証明書とシークレットの「値」>\"\n# OAuth2プロバイダからの認証応答を受け取るためのリダイレクトURLを指定します。\nredirect_url = \"https://webapps-php.example.com/oauth2/callback\"\n# Azure AD のテナントIDを指定します。\nazure_tenant = \"<Azure AD ディレクトリ (テナント) ID>\"\n# プロバイダの選択ボタンをスキップするかどうかを指定します。true が指定されている場合、ユーザーは直接プロバイダのログインページにリダイレクトされます。\nskip_provider_button = true\n```\n\n<blockquote class=\"warn\">\n<p>cookie_secret は、OAuth2 Proxy が発行する Cookie のシード(初期値)として使われます。</p>\n<p>24 バイトのランダムなバイト列を base64 エンコードしたものを設定しています。</p>\n<p>長すぎると、以下のエラーになります。</p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">[2023/12/12 20:17:11] [main.go:54] invalid configuration:</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\"> cookie_secret must be 16, 24, or 32 bytes to create an AES cipher, but is 44 bytes</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">Process 11727 has exited with status 1</span></p>\n<p><span style=\"color: #e70500;background-color: #ffebe7;\">Detaching</span></p>\n</blockquote>\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<p><span style=\"color: red;\"><strong>設定の場合:email_domains</strong></span></p>\n<p><span style=\"color: red;\"><strong>引数の場合:email-domain</strong></span></p>\n</blockquote>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image30.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image30.png\" alt=\"oauth2-proxy 設定 図\" width=\"851\" height=\"431\" loading=\"lazy\"></a>\n\n<br />\n\n# 動作確認\n\nF5 で起動します。\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image31.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image31.png\" alt=\"F5 で main.go を起動成功\" width=\"1023\" height=\"765\" loading=\"lazy\"></a>\n\n起動しました!\n\n<br />\n\n`https://webapps-php.example.com/info.php` にアクセスします。\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image32.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image32.png\" alt=\"info.phpにアクセス\" width=\"1209\" height=\"208\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image33.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image33.png\" alt=\"Azure AD認証1\" width=\"851\" height=\"668\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image34.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image34.png\" alt=\"Azure AD認証2\" width=\"847\" height=\"671\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image35.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/image35.png\" alt=\"info.php画面\" width=\"850\" height=\"298\" loading=\"lazy\"></a>\n\n<br />\n\n成功!ヨシっ!\n","description":"oauth2-proxy で既存アプリに Azure AD(Microsoft Entra ID) OpenID Connect 認証機能を追加しました。その全手順です。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"b6hstiec83","createdAt":"2024-01-06T09:34:15.935Z","updatedAt":"2024-01-06T09:34:15.935Z","publishedAt":"2024-01-06T09:34:15.935Z","revisedAt":"2024-01-06T09:34:15.935Z","url":"https://itc-engineering-blog.imgix.net/oauth2-proxy-azuread/ITC_Engineering_Blog.png","alt":"oauth2-proxyで既存アプリにAzure AD OpenID Connect認証機能を追加","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**1.** /tmp/test-softhsm-key-export ディレクトリを使って、トークンを初期化  \n**2.** RSA キーペア登録  \n**3.** 秘密鍵をラップ(キーを暗号化)して取り出す  \n**4.** 秘密鍵をアンラップ(キーを復号化してエクスポート)  \n**5.** (秘密鍵とペアの)公開鍵を使用してサンプルテキスト `\"key extracted!\\n\"` を暗号化  \n**6.** エクスポートした秘密鍵を DER 形式で /tmp/test-softhsm-key-export/sample.der に保存  \n**7.** `openssl` コマンドを使用して暗号文を復号化(このとき、6の sample.der を読み込む)  \n**8.** 復号化した結果 `\"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\nOK!!\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":"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 のインスタンス のリソースIDを格納します。\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;\">今回、コンテナが2つありますので、2つ並行して実行されます。</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 の2つのアプリ名は、`./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**1.** データ無しの状態で、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**2.** 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**3.** 再びデータを 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**4.** データを 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**5.** 再びデータを 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 を使ってデプロイ - 動作確認1~</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":"gitlab-as-openid-provider","createdAt":"2021-12-04T09:04:19.968Z","updatedAt":"2022-01-03T10:53:04.283Z","publishedAt":"2021-12-04T09:04:19.968Z","revisedAt":"2022-01-03T10:53:04.283Z","title":"GitLab as OpenID Connect identity providerをやってみた","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":"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":"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":"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://itc-engineering-blog.netlify.app/blogs/ad-fs-auth-openidc\" target=\"_blank\">AD FS 構成から mod_auth_openidc による OpenID Connect 認証成功まで全手順</a>」  \n「<a href=\"https://itc-engineering-blog.netlify.app/blogs/keycloak\" target=\"_blank\">Keycloak PostgreSQL OpenLDAP mod_auth_openidc で SSO 全手順</a>」  \nと AD FS、Keycloak を OpenID Provider として SSO(シングルサインオン)環境を構築してみましたが、今回、GitLab を OpenID Provider として SSO 環境を構築します。  \nGitLab 自体は、「<a href=\"https://itc-engineering-blog.netlify.app/blogs/fgm-dkbu10\" target=\"_blank\">Ubuntu 20.04.2.0 に GitLab をインストール</a>」の手順でインストール済みとします。  \nRelying Party(RP)側は、Apache のモジュールの`mod_auth_openidc`、`Flask`の拡張機能`Flask-OIDC`と2種類構築して、試してみました。\n\n<br />\n\n<blockquote class=\"info\">\n<p>【 mod_auth_openidc 】</p>\n<p>Apache 2.x HTTP ServerをOpenID ConnectのRelying Partyとして動作させる事を可能にする認証モジュールです。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 Flask 】</p>\n<p>Flask(フラスコ/フラスク)はPythonのWebアプリケーションフレームワークです。標準で提供する機能を最小限に保っているため、小規模なアプリに向いています。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 Flask-OIDC 】</p>\n<p>Flask-OIDCはFlaskの拡張機能であり、OpenID Connectベースの認証機能を数分でWebサイトに追加できます。</p>\n</blockquote>\n\n<br />\n\n**・GitLab の構造(OpenID Provider 関連部分のみ)**\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/GitLab_kouzou.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/GitLab_kouzou.png\" alt=\"GitLab の構造(OpenID Provider関連部分のみ)\" width=\"483\" height=\"252\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n※ものすごく簡略化しています。詳細は、こちらにあります。  \n<a href=\"https://docs.gitlab.com/ee/development/architecture.html#component-diagram\" target=\"_blank\">GitLab Application Architecture component diagram</a>\n\n<br />\n\n# 構成\n\n構成は、以下です。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/GitLab1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/GitLab1.png\" alt=\"GitLab mod_auth_openidc Flask-OIDC構成図\" width=\"538\" height=\"499\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n**`ユーザー`**  \nOS: Windows 10 PRO x64  \nChrome: バージョン: 96.0.4664.45(Official Build) (64 ビット)\n\n<br />\n\n**`OpenID Provider`**  \n以下の呼び方の場合もあります。  \nOpenID プロバイダー  \nOP  \nIdentity Provider  \nIdP\n\n<br />\n\nOS: Ubuntu 20.04.2 LTS  \nGitLab as OpenID Connect identity provider を構築するサーバーです。  \nホスト名は、gitlab-test.itccorporation.jp とします。  \n・GitLab v13.11.2  \n・Python 3.6.8\n\n<br />\n\n**`Relying Party1`**  \n以下の呼び方の場合もあります。  \nRP  \nService Provider  \nSP  \nClient  \nクライアント\n\n<br />\n\nOS: Raspberry Pi Desktop  \nDebian ベースの Linux です。  \n/etc/debian_version:10.7  \n・Apache/2.4.38 (Debian)  \n・mod_auth_openidc-2.3.10.2  \n・PHP 7.3.31-1~deb10u1 (cli)  \n ※php は、php である必要は無く、今回、認証が必要な Web アプリに見立てています。  \n・OpenLDAP 2.4.47(必須ではない)\n\n<br />\n\n**`Relying Party2`**  \nOS: CentOS Linux release 8.5.2111  \n`Flask-OIDC` をインストールするサーバーです。  \nアプリは、Python で、公式実装サンプルの`example.py`を使用します。\n\n<br />\n\n# GitLab https:// 化\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/GitLab2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/GitLab2.png\" alt=\"GitLab https:// 化 図\" width=\"538\" height=\"514\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<span style=\"background-color: cornsilk;\">**OpenID Provider(GitLab) での作業になります。**</span>\n\n<br />\n\n自己署名の SSL 証明書を作成します。  \n※「<a href=\"https://itc-engineering-blog.netlify.app/blogs/sslcert\">CentOS8 & Apache の自己署名証明書作成と証明書エラー回避</a>」に詳しい説明があります。\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) []:gitlab-test.itccorporation.jp\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:*.itccorporation.jp,IP:192.168.12.111\" > 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 = test.itccorporation.jp\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\n`gitlab.rb`を変更します。\n\n```shellsession\n# vi /etc/gitlab/gitlab.rb\n```\n\n```sh\nexternal_url 'http://gitlab-test.itccorporation.jp'\n↓\nexternal_url 'https://gitlab-test.itccorporation.jp'\n\nnginx['ssl_certificate'] = \"/etc/pki/tls/certs/ca.crt\"\nnginx['ssl_certificate_key'] = \"/etc/pki/tls/private/ca.key\"\n```\n\n<br />\n\n変更を反映して、再起動します。\n\n```shellsession\n# gitlab-ctl reconfigure\n# gitlab-ctl restart\n```\n\n`https://gitlab-test.itccorporation.jp/` にアクセス出来ればOKです。  \n<span style=\"color: red;\">注意:アクセスするのが早すぎると、`502 Whoops, GitLab is taking too much time to respond.`とエラーになるかもしれません。その場合、もう少し待つ必要があります。</span>\n\n<br />\n\n# GitLab https:// 化についての余談\n\nよく考えたら当たり前ですが、OpenID Provider(GitLab)の`https://`化は必須ですが、最初`http://`のままなんとかしようとして、ソースコードを書き換えて対応していました。\n\n```shellsession\n# vi /opt/gitlab/embedded/lib/ruby/gems/2.7.0/gems/doorkeeper-openid_connect-1.7.5/lib/doorkeeper/openid_connect/config.rb\n```\n\n```ruby\n  option :protocol, default: lambda { |*_|\n    ::Rails.env.production? ? :https : :http\n  }\n↓\n  option :protocol, default: lambda { |*_|\n    ::Rails.env.production? ? :http : :http\n  }\n```\n\nと書き換えて、  \n\n```shellsession\n# gitlab-ctl restart\n```\n\nGitLabリスタート後、  \n\n```shellsession\n# curl http://gitlab-test.itccorporation.jp/.well-known/openid-configuration\n```\n\nとすると、  \n`\"authorization_endpoint\": \"http://gitlab-test・・・`  \nのように各エンドポイントの情報が`http://`で返ってくるようになりましたが、mod_auth_openidc が頑なに`https://`でリダイレクトして、うまくいきませんでした。  \nmod_auth_openidc の設定を\n\n```apacheconf\nOIDCProviderAuthorizationEndpoint http://gitlab-test.itccorporation.jp/oauth/authorize\n```\n\nとすると、以下のように怒られました。\n\n`AH00526: Syntax error on line 838 of /etc/apache2/mods-enabled/auth_openidc.conf:`  \n`'http://gitlab-test.itccorporation.jp/oauth/authorize' cannot be parsed as a \"https\" URL (scheme == http)!`\n\n<br />\n\nそういった作業中に気付いたのですが、issuer の情報等は、設定で書き換えられます。(<a href=\"https://github.com/doorkeeper-gem/doorkeeper-openid_connect#configuration\" target=\"_blank\">doorkeeper-openid_connect</a>)\n\n```shellsession\n# vi /opt/gitlab/embedded/service/gitlab-rails/config/initializers/doorkeeper_openid_connect.rb\n```\n\n```ruby\n# frozen_string_literal: true\n\nDoorkeeper::OpenidConnect.configure do\n\n  #issuer Gitlab.config.gitlab.url\n  issuer 'dummy'\n```\n\n```shellsession\n# gitlab-ctl restart\n```\n\n`protocol`は設定では書き換えられませんでした。(GitLab がエラーになって起動しない。)\n\n```ruby\n# frozen_string_literal: true\n\nDoorkeeper::OpenidConnect.configure do\n  #protocol 'http'\n  protocol Gitlab.config.gitlab.protocol\n  issuer Gitlab.config.gitlab.url\n```\n\n<br />\n\n# GitLab アプリケーション設定\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/GitLab3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/GitLab3.png\" alt=\"GitLab アプリケーション設定 図\" width=\"538\" height=\"581\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<span style=\"background-color: cornsilk;\">**PC端末での作業になります。**</span>\n\n<br />\n\n管理者エリアから「アプリケーション」を選択し、「New application」ボタンをクリックします。  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image1.png\" alt=\"「New application」ボタンをクリック\" width=\"1218\" height=\"416\" loading=\"lazy\"></a>\n\n<br />\n\n**`Relying Party1`登録**  \nmod_auth_openidc のアプリケーションを登録します。\n\n<br />\n\n登録内容は以下とします。  \n名前: `testapp1`  \nRedirect URI: `https://192.168.12.166/oidc_callback`  \nTrusted: `チェック無し`  \n非公開: `チェック`  \nスコープ: `api`,`read_user`,`openid`,`email`にチェックとします。(<span style=\"color: red;\"><strong>`openid`は必須です。`email`はこの手順では必須です。</strong></span>)\n\n<br />\n\n名前は自由入力です。  \nRedirect URI は、アプリ(ここで言う`https://192.168.12.166/`側で使わないところを指定しないといけません。)  \nTrusted は、**チェックを入れないと**、ユーザー名、パスワード入力直後に以下の確認画面を表示します。  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image2.png\" width=\"534\" height=\"459\" alt=\"ユーザー名、パスワード入力直後の確認画面\" loading=\"lazy\"></a>  \n非公開は、おそらく、Keycloak で言う\"confidential\"に相当します。(詳細は割愛します。)今回チェックは必須です。  \nスコープは、アプリに渡して良い情報です。今回`openid`は必須です。(`api`にチェックを入れるとどうなるか等については割愛します。)\n\n<br />\n\n「Submit」をクリックします。  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image3.png\" alt=\"「Submit」をクリック1\" width=\"1289\" height=\"1046\" loading=\"lazy\"></a>\n\n<br />\n\n<span style=\"color: red;\"><strong>アプリケーション ID、秘密、コールバック URL は後で必要ですので、メモしておきます。</strong></span>\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image4.png\" alt=\"アプリケーション ID、秘密、コールバック URL\" width=\"853\" height=\"516\" loading=\"lazy\"></a>\n\n<br />\n\n**`Relying Party2`登録**  \n続けて、「New application」ボタンをクリックで、  \nFlask-OIDC のアプリケーションを登録します。\n\n<br />\n\n登録内容は以下とします。  \n名前: `testapp2`  \nRedirect URI: `http://localhost:5000/oidc_callback`  \nTrusted: `チェック無し`  \n非公開: `チェック`  \nスコープ: `api`,`read_user`,`openid`,`email`にチェックとします。(<span style=\"color: red;\"><strong>`openid`は必須です。`email`はこの手順では必須です。</strong></span>)  \n「Submit」をクリックします。\n\n<br />\n\n<span style=\"color: red;\"><strong>アプリケーション ID、秘密、コールバック URL は後で必要ですので、メモしておきます。</strong></span>\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image5.png\" alt=\"アプリケーション ID、秘密、コールバック URL\" width=\"863\" height=\"512\" loading=\"lazy\"></a>\n\n<br />\n\n他、アカウントを作成しておきます。\n\nこの例では、  \nName: Git Lab  \nUsername: gitlab  \nEmail: gitlabtestuser@contoso.com  \nとします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image6.png\" alt=\"アカウントを作成\" width=\"618\" height=\"352\" loading=\"lazy\"></a>\n\n<br />\n\n# mod_auth_openidc 設定\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/GitLab4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/GitLab4.png\" alt=\"mod_auth_openidc 設定 図\" width=\"538\" height=\"499\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<span style=\"background-color: cornsilk;\">**Relying Party(RP)1 での作業になります。**</span>\n\n<br />\n\nWeb アプリ側(`https://192.168.12.166/`)Relying Party(RP)の設定を行います。  \nApache2 のインストールから mod_auth_openidc の設定になりますが、内容が「AD FS 構成から mod_auth_openidc による OpenID Connect 認証成功まで全手順」の記事と重複するため、そちらをご確認ください。  \n<a href=\"https://itc-engineering-blog.netlify.app/blogs/ad-fs-auth-openidc#apache2\" target=\"_blank\">Apache2 インストール</a>  \n<a href=\"https://itc-engineering-blog.netlify.app/blogs/ad-fs-auth-openidc#mod_auth_openidc\" target=\"_blank\">mod_auth_openidc の設定</a>  \n↑  \n設定の説明はこちらにあります。\n\n<br />\n\nいきなり設定作業から入ります。  \n※GitLab サーバー`gitlab-test.itccorporation.jp`の名前解決ができない場合、hosts の登録が必要です。\n\n```shellsession\n# vi /etc/apache2/mods-available/auth_openidc.conf\n```\n\n```apacheconf\nOIDCRedirectURI https://192.168.12.166/oidc_callback\nOIDCCryptoPassphrase password\nOIDCProviderMetadataURL https://gitlab-test.itccorporation.jp/.well-known/openid-configuration\nOIDCScope \"openid email\"\nOIDCSSLValidateServer Off\nOIDCResponseType code\nOIDCClientID bf709b2bbc8f98d33bb770cb8533711ba67d6b1d3e675b0437e3fe837f26e13f\nOIDCClientSecret 74755c074a9622c9152adf80307e67d4e0f882e426f2169c759cf20af25e22c4\nOIDCPKCEMethod S256\nOIDCSessionInactivityTimeout 300\nOIDCHTMLErrorTemplate /etc/apache2/auth_openidc_error.html\nOIDCDefaultLoggedOutURL https://192.168.12.166/loggedout.html\nOIDCClaimPrefix OIDC_CLAIM_\nOIDCRemoteUserClaim nickname\nOIDCPassClaimsAs both\nOIDCAuthNHeader X-Remote-User\n```\n\nOIDCRedirectURI は、GitLab 設定中に設定した \"コールバック URL\" です。  \nOIDCProviderMetadataURL の`gitlab-test.itccorporation.jp`は、GitLab サーバー名です。  \nOIDCClientID は、GitLab 設定中に出てきた \"アプリケーション ID\" です。  \nOIDCClientSecret は、GitLab 設定中に出てきた \"秘密\" です。\n\n```shellsession\n# systemctl restart apache2\n```\n\n<br />\n\nUser:gitlab で`https://192.168.12.166/info.php`にアクセスして、ログインします。  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image7.png\" alt=\"アクセスして、ログイン1\" width=\"963\" height=\"505\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image8.png\" alt=\"アクセスして、ログイン2\" width=\"1208\" height=\"683\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image9.png\" alt=\"アクセスして、ログイン3\" width=\"1009\" height=\"487\" loading=\"lazy\"></a>\n\n<br />\n\nヨシ!\n\n<br />\n\n<span style=\"color: red;\">【エラーについて】</span>  \n`/var/log/apache2/error.log`:\n\n```sh\noidc_util_json_string_print: oidc_util_check_json_error: response contained an \"error\" entry with value: \"\"invalid_client\"\", referer: https://gitlab-test.itccorporation.jp/\noidc_util_json_string_print: oidc_util_check_json_error: response contained an \"error_description\" entry with value: \"\"Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.\"\", referer: https://gitlab-test.itccorporation.jp/\noidc_proto_resolve_code_and_validate_response: failed to resolve the code, referer: https://gitlab-test.itccorporation.jp/\n```\n\nOIDCClientSecret のクライアントIDが間違っています。\n\n<br />\n\n`/var/log/apache2/error.log`:\n\n```sh\noidc_proto_validate_exp: \"exp\" validation failure (1638407097): JWT expired 31 seconds ago, referer: https://gitlab-test.itccorporation.jp/\noidc_proto_parse_idtoken: id_token payload could not be validated, aborting, referer: https://gitlab-test.itccorporation.jp/\noidc_proto_validate_exp: \"exp\" validation failure (1638407098): JWT expired 30 seconds ago, referer: https://192.168.12.166/\noidc_proto_parse_idtoken: id_token payload could not be validated, aborting, referer: https://192.168.12.166/\n```\n\nmod_auth_openidc のサーバーと GitLab のサーバーとで時刻がズレすぎています。\n\n<br />\n\n`/var/log/apache2/error.log`:\n\n```sh\noidc_get_remote_user: JSON object did not contain a \"preferred_username\" string\noidc_set_request_user: OIDCRemoteUserClaimis set to \"preferred_username\", but could not set the remote user based on the requested claim \"preferred_username\" and the available claims for the user\noidc_handle_authorization_response: remote user could not be set\n```\n\nOIDCRemoteUserClaim の値が間違っています。(OIDCRemoteUserClaim `preferred_username`と GitLab から得られない claim を指定していた。)\n\n<br />\n\n# Flask-OIDC\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/GitLab5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/GitLab5.png\" alt=\"Flask-OIDC 図\" width=\"538\" height=\"568\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<span style=\"background-color: cornsilk;\">**Relying Party(RP)2 での作業になります。**</span>\n\n<br />\n\npython3 を使います。  \nインストールされていない場合、インストールします。\n\n```shellsession\n# dnf install python3\n```\n\n`flask`と`Flask-OIDC`をインストールします。\n\n```shellsession\n# pip3 install flask\n# pip3 install Flask-OIDC\n```\n\n<a href=\"https://github.com/puiterwijk/flask-oidc\" target=\"\\_blank\">`https://github.com/puiterwijk/flask-oidc`</a>  \nの`example.py`をコピーして設置します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image10.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image10.png\" alt=\"example.pyをコピーして設置\" width=\"1093\" height=\"508\" loading=\"lazy\"></a>\n\n```shellsession\n# mkdir /opt/flask-oidc\n# cd /opt/flask-oidc\n# vi example.py\n```\n\n<br />\n\n<span style=\"color: red;\"><strong>今回、簡略化のため、この`example.py`は極力変えない縛りでいきます。そのため、localhost:5000 で起動し、外部からアクセスできません。</strong></span>\n\n<br />\n\n`Flask-OIDC`の設定ファイル`client_secrets.json`を作成します。\n\n```shellsession\n# vi client_secrets.json\n```\n\n```json\n{\n  \"web\": {\n    \"issuer\": \"https://gitlab-test.itccorporation.jp/\",\n\n    \"client_id\": \"9676d08f8c249fbbc2a665a2cbf697c04416851667d058015931abd70da7c861\",\n    \"client_secret\": \"2a77f082fe7d30adf8f7a59c2f9eeb0d8db9af2bd98f361b0508c9d41deb9239\",\n    \"auth_uri\": \"https://gitlab-test.itccorporation.jp/oauth/authorize\",\n    \"redirect_urls\": [\"http://localhost:5000/oidc_callback\"],\n\n    \"token_uri\": \"https://gitlab-test.itccorporation.jp/oauth/token\",\n    \"userinfo_uri\": \"https://gitlab-test.itccorporation.jp/oauth/userinfo\"\n  }\n}\n```\n\n`gitlab-test.itccorporation.jp`部分は全て GitLab サーバー名です。  \nclient_id は、GitLab 設定中に出てきた \"アプリケーション ID\" です。  \nclient_secret は、GitLab 設定中に出てきた \"秘密\" です。  \nredirect_urls は、GitLab 設定中に設定した \"コールバック URL\" です。\n\n<br />\n\nこのまま起動すると、  \n`ssl.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:897)`  \nとなり、`https://gitlab-test.itccorporation.jp`への接続に失敗します。  \n<span style=\"color: red;\"><strong>`Flask-OIDC`が使っている`httplib2`のエラーで、`disable_ssl_certificate_validation=True`オプションを指定すれば良いのですが、ソースコードを書き換える必要があります。証明書を読み込ませるしか回避方法は無いようです。</strong></span>(実稼働を想定すると当たり前ですが。)\n\n<br />\n\n証明書管理システムの`certifi`を導入します。\n\n```shellsession\n# pip3 install certifi\n```\n\n<br />\n\nGitLab の証明書を取得します。\n\n```shellsession\n# openssl s_client -connect gitlab-test.itccorporation.jp:443 -showcerts\n```\n\nいろいろ表示されますが、\n\n`-----BEGIN CERTIFICATE-----`から`-----END CERTIFICATE-----`までを追記します。\n\n```shellsession\n# vi /usr/local/lib/python3.6/site-packages/certifi/cacert.pem\n```\n\n```sh\n-----BEGIN CERTIFICATE-----\nMIIDqDCCApCgAwIBAgIUGj8bA2ucR7TBvc59skeIZsuhTykwDQYJKoZIhvcNAQEL\n・・・(略)・・・\n6MhtLBxsJVsQZQAkn9UDNaNjK15Ui2CxfTroHQ==\n-----END CERTIFICATE-----\n```\n\n<br />\n\nまだハマりどころがあり、このまま起動すると、ログインした直後、  \n`return e_int.args[0].errno if isinstance(e_int.args[0], socket.error) else e_int.errno`  \n`IndexError: tuple index out of range`  \nエラーになります。\n\n<br />\n\n全く書き換えないでいきたかったですが、`example.py`を書き換えます。\n\n```shellsession\n# vi example.py\n```\n\n`import logging`の下に`import ssl`を追記します。\n\n```python\nimport logging\nimport ssl\n```\n\n<br />\n\n起動します。\n\n```shellsession\n# python3 example.py\n * Environment: production\n   WARNING: This is a development server. Do not use it in a production deployment.\n   Use a production WSGI server instead.\n * Debug mode: on\nINFO:werkzeug: * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)\nINFO:werkzeug: * Restarting with stat\nWARNING:werkzeug: * Debugger is active!\nINFO:werkzeug: * Debugger PIN: 135-432-255\n```\n\n<span style=\"color: red;\"><strong>`http://127.0.0.1:5000/`でバインドされていて、自分自身からしかアクセスできないため、CentOS8 GUI の Firefox でアクセスします。</strong></span>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image11.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image11.png\" alt=\"CentOS8 GUIのFirefoxでアクセス1\" width=\"795\" height=\"568\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image12.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image12.png\" alt=\"CentOS8 GUIのFirefoxでアクセス2\" width=\"795\" height=\"184\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image13.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image13.png\" alt=\"CentOS8 GUIのFirefoxでアクセス3\" width=\"795\" height=\"187\" loading=\"lazy\"></a>\n\n<br />\n\nヨシ!  \n(「None」と表示されているのは、`example.py`が`openid_id`を表示しようとしているためで、GitLab から`openid_id`claim は渡されないため、問題無しとします。)\n\n<br />\n\nシングルサインオンもヨシ!  \n※ログイン画面無しで遷移  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/sso_ok.gif\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/sso_ok.gif\" alt=\"シングルサインオン ログイン画面無しで遷移\" width=\"500\" height=\"231\" loading=\"lazy\"></a>\n\n<br />\n\n<span style=\"color: red;\">【エラーについて】</span>  \n端末:\n\n```sh\nERROR:flask_oidc:Token has expired\nDEBUG:flask_oidc:Invalid ID token\n```\n\n<br />\n\n画面(Not Aurhorized):  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image14.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image14.png\" alt=\"Not Aurhorized\" width=\"698\" height=\"166\" loading=\"lazy\"></a>\n\nとなった場合、  \nmod_auth_openidc の時と同様、時計を合わせる必要があります。\n\n<br />\n\n端末:\n\n```sh\nDEBUG:flask_oidc:Expired ID token, credentials missing\nTraceback (most recent call last):\n  File \"/usr/local/lib/python3.6/site-packages/flask_oidc/__init__.py\", line 437, in authenticate_or_redirect\n    self.credentials_store[id_token['sub']])\nKeyError: '9'\n```\n\n<br />\n\n画面(The requested scope is invalid, unknown, or malformed.):  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image15.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/image15.png\" alt=\"The requested scope is invalid, unknown, or malformed\" width=\"775\" height=\"246\" loading=\"lazy\"></a>\n\nとなった場合、  \n`example.py`が`email`を要求するのに対して、GitLab のアプリケーション設定のスコープで email のチェックが入っていない可能性があります。\n\n<br />\n","description":"GitLabをOpenID ProviderとしてSSO(シングルサインオン)環境を構築してみました。Relying Party(RP)側は、Apache のモジュールのmod_auth_openidc、Flaskの拡張機能Flask-OIDCです。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"kqmv7wazx5_j","createdAt":"2021-12-04T09:02:18.795Z","updatedAt":"2021-12-04T09:02:18.795Z","publishedAt":"2021-12-04T09:02:18.795Z","revisedAt":"2021-12-04T09:02:18.795Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/gitlab-as-openid-provider/ITC_Engineering_Blog.png","alt":"GitLab as OpenID Connect identity providerをやってみた","width":1200,"height":630}],"seo_authors":[]},{"id":"docker-win-gitlab","createdAt":"2022-01-21T14:12:45.735Z","updatedAt":"2022-01-31T13:16:30.563Z","publishedAt":"2022-01-21T14:12:45.735Z","revisedAt":"2022-01-31T13:16:30.563Z","title":"httpsのGitLabをDocker for Windowsでdocker run,docker-compose","category":{"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},"topics":[{"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":"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":"ag89wj9fc","createdAt":"2021-11-11T13:33:42.634Z","updatedAt":"2021-11-11T13:33:42.634Z","publishedAt":"2021-11-11T13:33:42.634Z","revisedAt":"2021-11-11T13:33:42.634Z","topics":"vmware","logo":"/logos/vmware.png","needs_title":false},{"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":"# はじめに\n\nDocker for Windows を VMware Workstation 16 の Windows10 PRO にインストールして、GitLab CE を docker run で起動してみました。さらに、docker-compose up でも起動してみました。今回、その手順を**Docker for Windows インストールのところから**書きたいと思います。\n\n<br />\n\n**目次**  \n1.[Docker for Windows インストール](#anchor1)  \n2.[データ領域変更 C:\\\\->D:\\\\](#anchor2)  \n3.[GitLab インストール(docker run 編)](#anchor3)  \n4.[GitLab インストール(docker-compose 編)](#anchor4)\n\n<br />\n\n<blockquote class=\"warn\">\n<p>【ホストPC検証環境】</p>\n<p><code>VMware Workstation 16 Pro</code></p>\n<p> <code>Windows 10 Pro 64-bit 20H2</code></p>\n<p>  <code>Docker Desktop 4.3.2 (72729)</code></p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>【Dockerコンテナ環境】</p>\n<p><code>GitLab CE v14.6.3</code></p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>この記事に「Dockerとは」「docker-composeとは」の説明はありませんが、<strong>知らないままでもできる</strong>と思います。</p>\n</blockquote>\n\n<br />\n\n<a class=\"anchor\" id=\"anchor1\"></a>\n\n# Docker for Windows インストール\n\nDocker Desktop for Windows を以下からダウンロードします。  \n<a href=\"https://hub.docker.com/editions/community/docker-ce-desktop-windows/\" target=\"_blank\">`https://hub.docker.com/editions/community/docker-ce-desktop-windows/`</a>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image1.png\" alt=\"Docker Desktop for Windows ダウンロード\" width=\"1061\" height=\"613\" loading=\"lazy\"></a>\n\n<br />\n\nダウンロードし終わったら、インストーラーを起動します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image2.png\" alt=\"Docker Desktop for Windows インストーラーを起動1\" width=\"109\" height=\"89\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image3.png\" alt=\"Docker Desktop for Windows インストーラーを起動2\" width=\"1387\" height=\"806\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image4.png\" alt=\"Docker Desktop for Windows インストーラーを起動3\" width=\"703\" height=\"484\" loading=\"lazy\"></a>\n\n<br />\n\nWindows を再起動します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image5.png\" alt=\"Docker Desktop for Windows インストーラー Windows を再起動\" width=\"701\" height=\"485\" loading=\"lazy\"></a>\n\n<br />\n\nDocker for Windows が起動してきて、以下のエラーになりました。\n\n<span style=\"color: red;background-color: cornsilk;\">WSL 2 installation is incomplete.</span>  \n<span style=\"color: red;background-color: cornsilk;\">The WSL 2 Linux kernel is now installed using a separate MSI update package.</span>  \n<span style=\"color: red;background-color: cornsilk;\">Please click the link and follow the instructions to install the kernel update:</span>  \n<span style=\"color: red;background-color: cornsilk;\">`https://aka.ms/wsl2kernel`.</span>  \n<span style=\"color: red;background-color: cornsilk;\">Press Restart after installing the Linux kernel.</span>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image6.png\" alt=\"Docker Desktop for Windows エラー1\" width=\"1268\" height=\"716\" loading=\"lazy\"></a>\n\n<br />\n\n`https://aka.ms/wsl2kernel`へアクセスすると、ダウンロードするリンクがあります。\n\n<blockquote class=\"info\">\n<p><code>https://aka.ms/wsl2kernel</code>のフルURLは、以下です。</p>\n<p><code>https://docs.microsoft.com/ja-jp/windows/wsl/install-manual#step-4---download-the-linux-kernel-update-package</code></p>\n</blockquote>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image7.png\" alt=\"wsl2kernel ダウンロードリンク\" width=\"1188\" height=\"285\" loading=\"lazy\"></a>\n\nダウンロードして、WSL 2 Linux kernel を更新します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image8.png\" alt=\"WSL 2 Linux kernel を更新1\" width=\"124\" height=\"78\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image9.png\" alt=\"WSL 2 Linux kernel を更新2\" width=\"492\" height=\"384\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image10.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image10.png\" alt=\"WSL 2 Linux kernel を更新3\" width=\"1097\" height=\"698\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image11.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image11.png\" alt=\"WSL 2 Linux kernel を更新4\" width=\"491\" height=\"385\" loading=\"lazy\"></a>\n\n<br />\n\n再び元のエラーダイアログに戻って、「Restart」をクリックします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image12.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image12.png\" alt=\"エラーダイアログに戻って、「Restart」\" width=\"1265\" height=\"716\" loading=\"lazy\"></a>\n\nまたエラーです。  \n<span style=\"color: red;background-color: cornsilk;\">System.InvalidOperationException:</span>  \n<span style=\"color: red;background-color: cornsilk;\">Failed to deploy distro docker-desktop to C:\\Users\\admin\\AppData\\Local\\Docker\\wsl\\distro: exit code: -1</span>  \n<span style=\"color: red;background-color: cornsilk;\"> stdout: Windows の仮想マシン プラットフォーム機能を有効にして、BIOS で仮想化が有効になっていることを確認してください。</span>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image13.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image13.png\" alt=\"Docker Desktop for Windows エラー2\" width=\"1264\" height=\"715\" loading=\"lazy\"></a>\n\n<span style=\"color: red;\"><strong>エラー文言の通り、原因は、Windows10 仮想マシンの BIOS で仮想化支援機能 \"ネストされた仮想化技術 (VT)\" が有効になっていないからでした。</strong></span>  \nどういうことかと言うと、仮想マシンの Windows10 の中に仮想化支援機能が必要な Docker をインストールしようとしているからです。  \n「Quit」をクリックして閉じます。\n\n<br />\n\n一旦シャットダウンして、該当 Windows10 仮想マシンの `*.vmx` をエディタで編集します。  \n`vhv.enable = \"TRUE\"`  \nの1行を追加します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image14.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image14.png\" alt=\"vmxファイル\" width=\"341\" height=\"67\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image15.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image15.png\" alt=\"vmxファイルをエディタで編集\" width=\"827\" height=\"96\" loading=\"lazy\"></a>\n\n<br />\n\n追加したら、仮想マシンを起動します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image16.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image16.png\" alt=\"Docker Desktop for Windowsインストール 完了\" width=\"915\" height=\"599\" loading=\"lazy\"></a>\n\nOKです!\n\n\"Skip tutorial\"をクリックして、先に進めます。(初めての場合、チュートリアルをやった方が良いと思いますが。)\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image17.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image17.png\" alt=\"Docker Desktop for Windowsインストール Skip tutorial\" width=\"1034\" height=\"606\" loading=\"lazy\"></a>\n\nこの状態になったら、インストール成功です。\n\n<br />\n\nもしかしたら、WSL 関係でいろいろ準備が必要かもしれませんが、今回の環境では、これで起動しました。\n\n例えば、\n\n```sh\n> wsl --status\n既定の配布: docker-desktop\n既定のバージョン: 2\n```\n\nで、`バージョン: 1` になっている場合、  \n`https://docs.microsoft.com/ja-jp/windows/wsl/install-manual` の  \n「手順 5 - WSL 2 を既定のバージョンとして設定する」\n\n```sh\n> wsl --set-default-version 2\n```\n\nが必要になるようです。\n\n<span style=\"color: red;\">「手順 6 - 選択した Linux ディストリビューションをインストールする」は、必要無かったです。</span>  \nUbuntu とかインストールする必要は有りません。\n\n```sh\n> wsl --list\nLinux 用 Windows サブシステム ディストリビューション:\ndocker-desktop (既定)\ndocker-desktop-data\n```\n\n<br />\n\n<a class=\"anchor\" id=\"anchor2\"></a>\n\n# データ領域変更 C:\\\\->D:\\\\\n\nC ドライブは容量が少なかったため、D ドライブに変更しました。**必須ではありません。**\n\n<br />\n\n初期状態の時、Docker のデータ領域は、  \n`%LOCALAPPDATA%\\Docker\\wsl\\data`  \n`%LOCALAPPDATA%\\Docker\\wsl\\distro`  \nです。\n\n<br />\n\n今回の場合、  \n`C:\\Users\\admin\\AppData\\Local\\Docker\\wsl\\data`  \n`C:\\Users\\admin\\AppData\\Local\\Docker\\wsl\\distro`  \nでした。\n\n<br />\n\n<blockquote class=\"info\">\n<p>何もしていない段階で、</p>\n<p>data: 747 MB</p>\n<p>distro: 137 MB</p>\n<p>でした。</p>\n</blockquote>\n\n`wsl --list`で表示される2つの distro(ディストリビューション)の内、  \n<span style=\"color: red;\"><strong>docker-desktop は \"bootstrapping distro\" で、distro フォルダ</strong></span>です。  \n<span style=\"color: red;\"><strong>docker-desktop-data は \"data store distro\" で、data フォルダ</strong></span>です。  \n参考:<a href=\"https://www.docker.com/blog/new-docker-desktop-wsl2-backend/\" target=\"_blank\">https://www.docker.com/blog/new-docker-desktop-wsl2-backend/</a>  \n※distro というのは、仮想マシンのような意味です。\n\n<br />\n\n今回、  \n`C:\\Users\\admin\\AppData\\Local\\Docker\\wsl\\data`  \n↓  \n`D:\\Docker\\wsl\\data`\n\n<br />\n\n`C:\\Users\\admin\\AppData\\Local\\Docker\\wsl\\distro`  \n↓  \n`D:\\Docker\\wsl\\distro`  \nと変更します。\n\n<br />\n\nクジラアイコンを右クリックして、  \n「Quit Docker Desktop」をクリックし、  \nDocker Desktop を停止します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image18.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image18.png\" alt=\"Quit Docker Desktop クジラアイコン\" width=\"172\" height=\"121\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image19.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image19.png\" alt=\"Quit Docker Desktop クリック\" width=\"326\" height=\"410\" loading=\"lazy\"></a>\n\n<blockquote class=\"info\">\n<p>クジラが潮を吹いている場合、裏で何かやっていますので、しばらく待つ必要があります。</p>\n</blockquote>\n\n```sh\n> wsl --list --verbose\n  NAME                   STATE           VERSION\n* docker-desktop         Stopped         2\n  docker-desktop-data    Stopped         2\n```\n\nStopped になっていたら、作業を続行して構いません。\n\n<br />\n\n`D:\\Docker\\wsl\\data`  \n`D:\\Docker\\wsl\\distro`  \nフォルダを作成します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image20.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image20.png\" alt=\"data distroフォルダ\" width=\"406\" height=\"144\" loading=\"lazy\"></a>\n\n<br />\n\n現在の distro をエクスポートします。\n\n```sh\n> wsl --export docker-desktop \"D:\\Docker\\wsl\\docker-desktop-distro.tar\"\n> wsl --export docker-desktop-data \"D:\\Docker\\wsl\\docker-desktop-data.tar\"\n```\n\n<br />\n\ndistro を削除します。\n\n```sh\n> wsl --unregister docker-desktop\n> wsl --unregister docker-desktop-data\n```\n\n<br />\n\n`%LocalAppData%\\Docker\\wsl\\distro\\ext4.vhdx`  \n`%LocalAppData%\\Docker\\wsl\\data\\ext4.vhdx`  \nが削除されます。\n\n<br />\n\nインポートします。\n\n```sh\n> wsl --import docker-desktop \"D:\\Docker\\wsl\\distro\" \"D:\\Docker\\wsl\\docker-desktop-distro.tar\" --version 2\n> wsl --import docker-desktop-data \"D:\\Docker\\wsl\\data\" \"D:\\Docker\\wsl\\docker-desktop-data.tar\" --version 2\n```\n\n<br />\n\nインポートされたか確認します。\n\n```sh\n> wsl --list --verbose\n  NAME                   STATE           VERSION\n* docker-desktop         Stopped         2\n  docker-desktop-data    Stopped         2\n```\n\ndocker-desktop と docker-desktop-data、2つ表示されたら成功です。  \nDocker を起動します。\n\n```sh\n> \"C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe\"\n```\n\n<br />\n\n<a class=\"anchor\" id=\"anchor3\"></a>\n\n# GitLab インストール(docker run 編)\n\n## コンテナ作成\n\nボリュームを作成します。\n\n```sh\n> docker volume create gitlab-data-vol\n> docker volume create gitlab-log-vol\n> docker volume create gitlab-config-vol\n```\n\ngitlab-ce 最新版コンテナを作成、起動します。コンテナの名前は、gitlab です。\n\n```sh\n> docker run --detach ^\n  --publish 443:443 --publish 80:80 --publish 22:22 ^\n  --name gitlab ^\n  --restart always ^\n  --volume gitlab-config-vol:/etc/gitlab ^\n  --volume gitlab-log-vol:/var/log/gitlab ^\n  --volume gitlab-data-vol:/var/opt/gitlab ^\n  gitlab/gitlab-ce:latest\n```\n\n<blockquote class=\"warn\">\n<p>cmd.exeのコマンド例です。ハット <code>^</code> の部分は、PowerShellの場合、バッククォート <code>`</code> です。</p>\n</blockquote>\n\n```sh\nDigest: sha256:fcfd3bf76c60891fd8400e2984b706083739d49127b20eb788cd292b5e94846f\nStatus: Downloaded newer image for gitlab/gitlab-ce:latest\n82d1b92b45181cc79e17bc2bcda5d9774eb7a904db2285d50ff6631fececd1e4\ndocker: Error response from daemon: Ports are not available: listen tcp 0.0.0.0:80: bind: An attempt was made to access a socket in a way forbidden by its access permissions.\n```\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image21.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image21.png\" alt=\"docker run 80ポート エラー\" width=\"547\" height=\"176\" loading=\"lazy\"></a>\n\n作成には成功しましたが、エラーが表示されて起動に失敗しました。\n\n<br />\n\n<span style=\"color: red;\">Windows の「World Wide Web 発行サービス」が 80 番ポートを使用しているため</span>でした。  \nサービス管理画面で、停止、かつ、Windows 再起動時にサービスが再び立ち上がってこないように、無効にしておきます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image22.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image22.png\" alt=\"「World Wide Web 発行サービス」停止 無効1\" width=\"1194\" height=\"621\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image23.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image23.png\" alt=\"「World Wide Web 発行サービス」停止 無効2\" width=\"467\" height=\"529\" loading=\"lazy\"></a>\n\n<blockquote class=\"info\">\n<p>タスクマネージャーで見る場合、</p>\n<p>名前: W3SVC</p>\n<p>説明: World Wide Web 発行サービス</p>\n<p>グループ: iissvcs</p>\n<p>が同じサービスを指しています。</p>\n</blockquote>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image24.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image24.png\" alt=\"「World Wide Web 発行サービス」タスクマネージャー\" width=\"880\" height=\"542\" loading=\"lazy\"></a>\n\n<blockquote class=\"warn\">\n<p>該当のサービス名が異なるなど、いろいろなケースがあると思いますが、今回は、それだけで続行できました。</p>\n</blockquote>\n\n<br />\n\nコンテナは作られたので、`run`ではなく、`start`で起動します。\n\n```sh\n> docker start gitlab\n```\n\n<br />\n\n## ホスト名設定\n\nコンテナの中に入ります。コンテナ名は、\"gitlab\"です。\n\n```\n> docker exec -it gitlab /bin/bash\n```\n\n<br />\n\nGitLab の設定を変更します。\n\n```shell-session\n# vi /etc/gitlab/gitlab.rb\n```\n\nGitLab のホスト名を設定します。\n\n<blockquote class=\"info\">\n<p>今回、</p>\n<p>ホスト名: <code>gitlab-9.itccorporation.jp</code></p>\n<p>IPアドレス: <code>192.168.11.9</code></p>\n<p>とします。</p>\n</blockquote>\n\n```ruby\n# url and port of http(s) access\nexternal_url 'http://gitlab-9.itccorporation.jp/'\n# ssh access address, ssh port has another configuration item\ngitlab_rails['gitlab_ssh_host'] = '192.168.11.9'\n```\n\n設定を反映します。\n\n```shell-session\n# gitlab-ctl reconfigure\n# gitlab-ctl restart\n```\n\n<br />\n\n`http://gitlab-9.itccorporation.jp/`にアクセスして、ここまでで問題無いか確認してみます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image25.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image25.png\" alt=\"docker run httpでGitLabにアクセス\" width=\"978\" height=\"631\" loading=\"lazy\"></a>\n\nヨシ!\n\n<br />\n\n## メール設定\n\nメールの設定を行います。\n\n```shell-session\n# vi /etc/gitlab/gitlab.rb\n```\n\n<blockquote class=\"warn\">\n<p>設定は一例です。gmailの設定は、別記事</p>\n<p>「<a href=\"https://itc-engineering-blog.netlify.app/blogs/fgm-dkbu10#h2-titile-13\" target=\"_blank\">Ubuntu 20.04.2.0にGitLabをインストール</a>」にあります。</p>\n<p>メールを利用しない場合は、スキップしても構いません。</p>\n</blockquote>\n\n```ruby\ngitlab_rails['smtp_enable'] = true\ngitlab_rails['smtp_address'] = \"mail.example.com\"\ngitlab_rails['smtp_port'] = 587\ngitlab_rails['smtp_user_name'] = \"foobar\"\ngitlab_rails['smtp_password'] = \"password\"\ngitlab_rails['smtp_domain'] = \"mail.example.com\"\ngitlab_rails['smtp_authentication'] = \"plain\"\ngitlab_rails['smtp_enable_starttls_auto'] = false\ngitlab_rails['smtp_tls'] = false\n\ngitlab_rails['gitlab_email_from'] = 'gitlab-docker@example.com'\ngitlab_rails['gitlab_email_reply_to'] = 'gitlab-docker@example.com'\n```\n\n設定を反映します。\n\n```shell-session\n# gitlab-ctl reconfigure\n```\n\n<br />\n\nメール設定が問題無いか確認します。\n\n```shell-session\n# gitlab-rails console\n```\n\n<br />\n\nsmtp が有効かどうか確認します。以下のように表示されれば、問題ありません。\n\n```sh\n> ActionMailer::Base.delivery_method\n=> :smtp\n```\n\n<br />\n\nメール送信設定の設定状況を確認します。以下のように表示されれば、問題ありません。\n\n```sh\n> ActionMailer::Base.smtp_settings\n=> {:authentication=>:plain, :user_name=>\"foobar\", :password=>\"password\", :address=>\"mail.example.com\", :port=>587, :domain=>\"mail.example.com\", :enable_starttls_auto=>false, :tls=>false, :ca_file=>\"/opt/gitlab/embedded/ssl/certs/cacert.pem\"}\n```\n\n<br />\n\nメール送信テストをします。以下のように表示されて、実際にメールを受け取れれば問題ありません。\n\n```sh\n> Notify.test_email('foobar@example.com', 'Message Subject', 'Message Body').deliver_now\nDelivered mail 61e65ef22a378_1b6e5adc594e7@94228cb76ed1.mail (611.5ms)\n=> #<Mail::Message:154840, Multipart: false, Headers: <Date: Tue, 18 Jan 2022 14:39:18 +0000>, <From: GitLab <gitlab-docker@example.com>>, <Reply-To: GitLab <gitlab-docker@example.com>>, <To: foobar@example.com>, <Message-ID: <61e65ef22a378_1b6e5adc594e7@94228cb76ed1.mail>>, <Subject: Message Subject>, <Mime-Version: 1.0>, <Content-Type: text/html; charset=UTF-8>, <Content-Transfer-Encoding: 7bit>, <Auto-Submitted: auto-generated>, <X-Auto-Response-Suppress: All>>\n```\n\n<br />\n\n## SSL 設定\n\n`https://`で稼働するように設定していきます。\n\n<br />\n\n自己署名証明書を作成します。  \n詳しい説明は省略します。詳しい説明は、別記事「<a href=\"https://itc-engineering-blog.netlify.app/blogs/sslcert\" target=\"_blank\">CentOS8 & Apache の自己署名証明書作成と証明書エラー回避</a>」にあります。\n\n```shell-session\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) []:gitlab-9.itccorporation.jp\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:*.itccorporation.jp,IP:192.168.11.9\" > 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-9.itccorporation.jp\nGetting Private key\n# mkdir /etc/gitlab/ssl\n# mv ca.crt /etc/gitlab/ssl/gitlab-9.itccorporation.jp.crt\n# mv ca.key /etc/gitlab/ssl/gitlab-9.itccorporation.jp.key\n# mv ca.csr /etc/gitlab/ssl/gitlab-9.itccorporation.jp.csr\n```\n\nURL を`https://`に変更します。\n\n```shell-session\n# vi /etc/gitlab/gitlab.rb\n```\n\n```ruby\nexternal_url 'https://gitlab-9.itccorporation.jp'\n```\n\nさらに以下も設定します。\n\n```ruby\nnginx['redirect_http_to_https'] = true\n\nnginx['ssl_certificate'] = \"/etc/gitlab/ssl/gitlab-9.itccorporation.jp.crt\"\nnginx['ssl_certificate_key'] = \"/etc/gitlab/ssl/gitlab-9.itccorporation.jp.key\"\n```\n\n設定を反映します。\n\n```shell-session\n# gitlab-ctl reconfigure\n# gitlab-ctl restart\n```\n\n<br />\n\n`https://gitlab-9.itccorporation.jp/`にアクセスして、ここまでで問題無いか確認してみます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image25.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image25.png\" alt=\"docker run httpsでGitLabにアクセス\" width=\"978\" height=\"631\" loading=\"lazy\"></a>\n\nヨシ!\n\n<br />\n\n## root パスワード変更\n\nroot ユーザーの初期パスワードを確認します。\n\n```shell-session\n# cat /etc/gitlab/initial_root_password\n```\n\n```sh\n# WARNING: This value is valid only in the following conditions\n#          1. If provided manually (either via `GITLAB_ROOT_PASSWORD` environment variable or via `gitlab_rails['initial_root_password']` setting in `gitlab.rb`, it was provided before database was seeded for the first time (usually, the first reconfigure run).\n#          2. Password hasn't been changed manually, either via UI or via command line.\n#\n#          If the password shown here doesn't work, you must reset the admin password following https://docs.gitlab.com/ee/security/reset_user_password.html#reset-your-root-password.\n\nPassword: PFwQfi9XG1/JMMgeHBRefu7uAVMY273nKX4m8D2C1J0=\n```\n\nのように表示されますので、これが初期パスワードになります。\n\n<br />\n\nUser: `root`  \nPassword: `PFwQfi9XG1/JMMgeHBRefu7uAVMY273nKX4m8D2C1J0=`  \n(イコール記号を含めてパスワード。)\n\n<br />\n\nでログインします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image26.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image26.png\" alt=\"GitLab rootログイン\" width=\"999\" height=\"649\" loading=\"lazy\"></a>\n\n<br />\n\n右上アイコンクリック → Edit profile → Password  \nで任意のパスワードに変更しておきます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image27.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image27.png\" alt=\"右上アイコンクリック → Edit profile → Password1\" width=\"474\" height=\"347\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image28.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image28.png\" alt=\"右上アイコンクリック → Edit profile → Password2\" width=\"480\" height=\"452\" loading=\"lazy\"></a>\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image29.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image29.png\" alt=\"右上アイコンクリック → Edit profile → Password3\" width=\"1103\" height=\"470\" loading=\"lazy\"></a>\n\n以上です。\n\n<br />\n\n次は、<span style=\"color: red\">docker-compose で**同じこと**を行います。</span>\n\n<br />\n\n<a class=\"anchor\" id=\"anchor4\"></a>\n\n# GitLab インストール(docker-compose 編)\n\nカレントフォルダに  \n`gitlab-9.itccorporation.jp.crt`  \n`gitlab-9.itccorporation.jp.key`  \n`gitlab-9.itccorporation.jp.csr`  \nがあるものとします。(どこかで作成)\n\n<br />\n\n`docker-compose.yaml`を作成します。  \nこれは、ここまでやってきたこと全てが記述されています。  \nなお、root のパスワードは、  \n`gitlab_rails['initial_root_password'] = 'adminadmin'`  \nで任意のパスワードにできます。\n\n```yaml\nversion: '3.6'\nservices:\n  web:\n    image: 'gitlab/gitlab-ce:latest'\n    restart: always\n    hostname: 'gitlab-9.itccorporation.jp'\n    environment:\n      GITLAB_OMNIBUS_CONFIG: |\n        external_url 'https://gitlab-9.itccorporation.jp'\n        gitlab_rails['gitlab_ssh_host'] = '192.168.11.9'\n        gitlab_rails['smtp_enable'] = true\n        gitlab_rails['smtp_address'] = \"mail.example.com\"\n        gitlab_rails['smtp_port'] = 587\n        gitlab_rails['smtp_user_name'] = \"foobar\"\n        gitlab_rails['smtp_password'] = \"password\"\n        gitlab_rails['smtp_domain'] = \"mail.example.com\"\n        gitlab_rails['smtp_authentication'] = \"plain\"\n        gitlab_rails['smtp_enable_starttls_auto'] = false\n        gitlab_rails['smtp_tls'] = false\n        gitlab_rails['gitlab_email_from'] = 'gitlab-docker@example.com'\n        gitlab_rails['gitlab_email_reply_to'] = 'gitlab-docker@example.com'\n        gitlab_rails['initial_root_password'] = 'adminadmin'\n        nginx['redirect_http_to_https'] = true\n        nginx['ssl_certificate'] = \"/etc/gitlab/ssl/gitlab-9.itccorporation.jp.crt\"\n        nginx['ssl_certificate_key'] = \"/etc/gitlab/ssl/gitlab-9.itccorporation.jp.key\"\n    ports:\n      - '80:80'\n      - '443:443'\n      - '22:22'\n    volumes:\n      - './config:/etc/gitlab'\n      - './logs:/var/log/gitlab'\n      - './data:/var/opt/gitlab'\n      - './gitlab-9.itccorporation.jp.crt:/etc/gitlab/ssl/gitlab-9.itccorporation.jp.crt'\n      - './gitlab-9.itccorporation.jp.key:/etc/gitlab/ssl/gitlab-9.itccorporation.jp.key'\n      - './gitlab-9.itccorporation.jp.csr:/etc/gitlab/ssl/gitlab-9.itccorporation.jp.csr'\n    shm_size: '256m'\n```\n\n<br />\n\nイメージ作成、コンテナ作成、起動します。\n\n```sh\n> docker-compose up -d\n```\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image30.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image30.png\" alt=\"docker-compose up\" width=\"477\" height=\"301\" loading=\"lazy\"></a>\n\n<blockquote class=\"warn\">\n<p>当たり前ですが、docker runで作成したコンテナを止めておかないとポートが確保できず、エラーになります。</p>\n</blockquote>\n\n↓\n\n```sh\n> docker-compose up\nCreating network \"gitlab_default\" with the default driver\nCreating gitlab_web_1 ...\nCreating gitlab_web_1 ... error\n\nERROR: for gitlab_web_1  Cannot start service web: driver failed programming external connectivity on endpoint gitlab_web_1 (da056cd5d9931facd04ae01ac92fde704fd24fef172c3758025f74acad514528): Bind for 0.0.0.0:443 failed: port is already allocated\n\n> docker stop gitlab\n> docker-compose up\n```\n\n<br />\n\n`https://gitlab-9.itccorporation.jp/ `にアクセスします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image25.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/image25.png\" alt=\"docker-compose httpsでGitLabにアクセス\" width=\"978\" height=\"631\" loading=\"lazy\"></a>\n\n<br />\n\nヨシ!!\n\n<br />\n\n<blockquote class=\"warn\">\n<p>アクセスするのが早すぎると、接続に失敗します。アクセスできるようになるまで数分かかるかもしれません。</p>\n<p>以下のようなログの出力があるまで待つ必要があります。</p>\n</blockquote>\n\n↓\n\n```sh\n==> /var/log/gitlab/gitlab-exporter/current <==\n2022-01-20_13:24:37.29381 Passing 'exists?' command to redis as is; blind passthrough has been deprecated and will be removed in redis-namespace 2.0 (at /opt/gitlab/embedded/lib/ruby/gems/2.7.0/gems/sidekiq-6.3.1/lib/sidekiq/api.rb:960:in `block (3 levels) in each')\n2022-01-20_13:24:37.35583 127.0.0.1 - - [20/Jan/2022:13:24:36 UTC] \"GET /sidekiq HTTP/1.1\" 200 69839\n2022-01-20_13:24:37.35614 - -> /sidekiq\nok: down: alertmanager: 0s, normally up\n\n==> /var/log/gitlab/gitaly/current <==\n{\"gitaly\":1402,\"level\":\"warning\",\"msg\":\"forwarding signal\",\"pid\":1494,\"signal\":15,\"time\":\"2022-01-20T13:24:37.593Z\",\"wrapper\":1494}\n{\"gitaly\":1402,\"level\":\"warning\",\"msg\":\"forwarding signal\",\"pid\":1494,\"signal\":18,\"time\":\"2022-01-20T13:24:37.594Z\",\"wrapper\":1494}\n```\n\n<br />\n\n<blockquote class=\"warn\">\n<p><code>gitlab_rails['initial_root_password'] = 'adminadmin'</code> は <code>docker-compose up</code> の後に変更できません。変更する場合は、コンテナ、イメージ、config、logs、<strong>data</strong>削除後、<code>docker-compose up</code> やり直しが必要です。</p>\n</blockquote>\n","description":"httpsのGitLabをDocker for Windowsでdocker runとdocker-composeの2方式で構築しました。VMware Workstation 16のWindows10仮想マシンにDockerをインストールするところからの手順です。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"9uuap1zw4_o","createdAt":"2022-01-21T14:09:56.611Z","updatedAt":"2022-01-21T14:09:56.611Z","publishedAt":"2022-01-21T14:09:56.611Z","revisedAt":"2022-01-21T14:09:56.611Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/docker-win-gitlab/ITC_Engineering_Blog.png","alt":"httpsのGitLabをDocker for Windowsでdocker run,docker-compose","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":"1ff2yl5tu","createdAt":"2021-05-08T09:47:47.919Z","updatedAt":"2023-11-11T11:07:33.156Z","publishedAt":"2021-05-08T09:47:47.919Z","revisedAt":"2023-11-11T11:07:33.156Z","title":"CentOS8にwekanをインストール(ソースからビルド編)","category":{"id":"gg2a3kv3ofiu","createdAt":"2021-05-09T08:35:47.263Z","updatedAt":"2021-05-09T08:35:47.263Z","publishedAt":"2021-05-09T08:35:47.263Z","revisedAt":"2021-05-09T08:35:47.263Z","topics":"Wekan","logo":"/logos/Wekan.png","needs_title":false},"topics":[{"id":"gg2a3kv3ofiu","createdAt":"2021-05-09T08:35:47.263Z","updatedAt":"2021-05-09T08:35:47.263Z","publishedAt":"2021-05-09T08:35:47.263Z","revisedAt":"2021-05-09T08:35:47.263Z","topics":"Wekan","logo":"/logos/Wekan.png","needs_title":false},{"id":"n767cc7fin","createdAt":"2021-02-18T07:12:11.544Z","updatedAt":"2021-08-31T12:09:10.575Z","publishedAt":"2021-02-18T07:12:11.544Z","revisedAt":"2021-08-31T12:09:10.575Z","topics":"CentOS","logo":"/logos/CentOS.png","needs_title":false},{"id":"r6dlrubh_xa","createdAt":"2021-05-09T08:35:00.898Z","updatedAt":"2021-05-09T08:35:00.898Z","publishedAt":"2021-05-09T08:35:00.898Z","revisedAt":"2021-05-09T08:35:00.898Z","topics":"MongoDB","logo":"/logos/MongoDB.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}],"content":"# はじめに\n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;\"><strong>【2023年11月更新】</strong></span></p>\n<p>ブログ投稿当時の手順のままの場合、途中でエラーになることが分かりました。そのため、ところどころ書き換えました。</p>\n<p>当時の手順を残しつつ修正したため、一旦エラーになり、対応する手順になります。</p>\n</blockquote>\n\nWekan Open-Source kanban  \nをソースコードを使ってインストールしてみました。  \n<br />\nWekanは、OSSのかんばん管理ツールです。社内タスクの管理、見える化に役立ちます。  \nソースコードは、Node.js、Meteorフレームワークで構成されています。MITライセンスです。  \n同じ目的の場合、Trelloが有名ですが、閉じた環境で自力運用したくて、Wekanを選びました。  \n<br />\nインストール環境:CentOS Linux release 8.3.2011 (VMware上、インターネット接続あり)  \n<br />\n別記事<a href=\"https://itc-engineering-blog.netlify.app/blogs/knfpjkzmc1\" target=\"_blank\">「CentOS8にwekanをインストール(snap編)」</a>では、snapを使い、インストールしましたが、今回は、snapを使わず、dockerでもなく、ソースからインストールしていきます。  \n<a href=\"https://github.com/wekan/wekan\" target=\"_blank\">github.com/wekan</a> から rebuild-wekan.sh というのを見つけて、この内容を行えばできそうと思い、やってみました。  \n基本的に rebuild-wekan.sh の内容通りでしたが、今回の環境の場合、CentOSのため、rebuild-wekan.sh の序盤 apt-get でいきなりコケますので、dnf, yumで代替して進めています。\n\n<br />\n\n# インストール準備\n  \n<blockquote class=\"warn\">\n<p>root権限で作業していますので、全てsudoは省略しています。</p>\n</blockquote>\n\n<br />\n\nパッケージを更新します。  \n\n```shellsession\n# sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*\n# dnf -y upgrade\n```\n\n<br />\n\ngit cloneでソースコードをコピーしたいので、gitをインストールします。\n\n```shellsession\n# dnf install git\nIs this ok [y/N]: y\n```\n※以降基本的にyのため、-yを付けます。  \n -y は、? [y/N]: のようなときに自動的に y とするオプションです。  \n\n<blockquote class=\"info\">\n<p>dnf = Dandified Yum(ダンディファイド ヤム、略してDNF)</p>\n<p>は、RPMベースのパッケージ管理システムを採用しているLinuxディストリビューション用のパッケージマネージャであるYum 3.4のフォークであり、Yumの事実上の後継バージョンです。(Wikipediaより)</p>\n<p>Dandified = いきにめかしこんだ、しゃれこんだ</p>\n<p>という意味になります。</p>\n</blockquote>\n\n<br />\n\nwekanのソースコードを取得します。\n\n```shellsession\n# cd /opt\n# git clone https://github.com/wekan/wekan.git\n# cd wekan\n```\n\n<br />\n\n# Wekan依存関係インストール\n\nrebuild-wekan.sh では、  \n`sudo apt-get install -y build-essential gcc g++ make git curl wget npm p7zip-full`  \nとなっていますので、それに相当することをやっていきます。  \n<br />\n開発ツールをまとめてインストールします。\n\n```shellsession\n# yum -y groupinstall \"Development Tools\"\n```\n\n<blockquote class=\"info\">\n<p>UbuntuやDebianのbuild-essential に相当するようです。</p>\n<p>autoconf, automake, gcc-c++ ... 等々、開発用のツールが大量にインストールされます。</p>\n<p>※今回は最初にdnfでインストールしましたが、ここでgitも入るようです。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>p7zip-fullに関して、rebuild-wekan.shでは、</p>\n<pre><code>#curl https://releases.wekan.team/fibers-multi.7z -o fibers-multi.7z\n#7z x fibers-multi.7z\n#rm fibers-multi.7z</code></pre>\n<p>とコメントアウトされていますので、インストールしませんでした。</p>\n</blockquote>\n\n<br />\n\nnpmをインストールします。\n\n```shellsession\n# dnf -y install npm\n# npm -v\n6.14.11\n```\n<blockquote class=\"info\">\n<p>npmは、Node.jsのパッケージ管理システムです。</p>\n</blockquote>\n\n<br />\n\nnodeをインストールします。\n\n```shellsession\n# npm -g install n\n# n 12.22.1\n```\n<blockquote class=\"info\">\n<p>nは、Node.js(node)のバージョンを管理するツールです。</p>\n<p>今回、12.22.1 を指定しています。(rebuild-wekan.shがそうなっているため。)</p>\n</blockquote>\n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;\"><strong>【2023年11月更新】</strong></span></p>\n<p>ブログ投稿当時の手順のままの場合、12.22.1 です。この後、14.21.4(wekan/node-v14-esm) に更新します。</p>\n</blockquote>\n\n<br />\n\nnode-gypをインストールします。\n\n```shellsession\n# npm -g install node-gyp\n```\n\n<blockquote class=\"info\">\n<p>node-gyp(gyp=Generate Your Projects)は、Node.jsのネイティブアドオンモジュールをコンパイルするために、Node.jsで書かれたクロスプラットフォームのコマンドラインツールです。</p>\n<p>ネイティブアドオンモジュールとは、Node.js本体を拡張するプログラム(C/C++)となります。</p>\n</blockquote>\n\n<br />\n\nfibersをインストールします。\n\n```shellsession\n# mkdir -p /usr/local/lib/node_modules/fibers/.node-gyp\n# npm -g install fibers\n```\n\n<blockquote class=\"info\">\n<p>fibersというのは、Node.jsの非同期処理用のライブラリです。今回は、これがNode.jsのネイティブアドオンモジュールにあたるようです。</p>\n</blockquote>\n\n<br />\n\nMeteorをインストールします。\n\n```shellsession\n# curl https://install.meteor.com | bash\n```\n\n<blockquote class=\"info\">\n<p>https://install.meteor.comの内容は、シェルスクリプトで、ダイレクトに実行しているのですが、ファイル(meteor-bootstrap-${PLATFORM}.tar.gz)をダウンロードするところが有り、タイミングによるかもしれませんが、ここで、かなり時間がかかりました。(1時間くらい)</p>\n</blockquote>\n\n<br />\n\n# Wekanのビルド\n\nmeteor buildコマンドでWekanをビルドします。\n\n```shellsession\n# rm -rf node_modules .meteor/local .build\n# chmod u+w *.json\n# npm install\n# meteor build .build --directory --allow-superuser\n```\n\n<blockquote class=\"info\">\n<p>rootユーザーの場合、</p>\n<p>--allow-superuser</p>\n<p>を付けないとエラーになりますので、付けて実行します。</p>\n</blockquote>\n\n<blockquote class=\"alert\">\n<p>meteor buildは、かなりメモリを消費するようです。メモリ1GBで行ったときは、処理が遅延し、2時間以上かかりました。メモリ4GB程度にすることをお勧めします。</p>\n</blockquote>\n\n<br />\n\nゴミ掃除します。\n\n```shellsession\n# rm -rf .build/bundle/programs/web.browser.legacy\n# cd .build/bundle/programs/server\n# rm -rf node_modules\n# chmod u+w *.json\n# npm install\nERROR: npm v10.2.3 is known not to run on Node.js v12.22.1.  This version of npm supports the following node versions: `^18.17.0 || >=20.5.0`. You can find the latest version at https://nodejs.org/.\n\nERROR:\n/usr/local/lib/node_modules/npm/lib/utils/exit-handler.js:19\n  const hasLoadedNpm = npm?.config.loaded\n                           ^\n\nSyntaxError: Unexpected token '.'\n    at wrapSafe (internal/modules/cjs/loader.js:915:16)\n    at Module._compile (internal/modules/cjs/loader.js:963:27)\n    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)\n    at Module.load (internal/modules/cjs/loader.js:863:32)\n    at Function.Module._load (internal/modules/cjs/loader.js:708:14)\n    at Module.require (internal/modules/cjs/loader.js:887:19)\n    at require (internal/modules/cjs/helpers.js:74:18)\n    at module.exports (/usr/local/lib/node_modules/npm/lib/cli-entry.js:15:23)\n    at module.exports (/usr/local/lib/node_modules/npm/lib/es6/validate-engines.js:39:10)\n    at module.exports (/usr/local/lib/node_modules/npm/lib/cli.js:4:31)\n```\nエラーになりました。  \n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;\"><strong>【2023年11月更新】</strong></span></p>\n<p>ブログ投稿当時の手順のままの場合、ここでエラーになりました。14.21.4(wekan/node-v14-esm) に更新します。</p>\n<p>14.21.4(wekan/node-v14-esm) に更新する理由は、rebuild-wekan.sh の中で同じことを行っているからです。</p>\n<p>エラー内容は、<code>^18.17.0 || >=20.5.0</code> ですが、v14 でエラーになりませんでした。</p>\n</blockquote>\n\n<br />\n\nnode v14 に切り替えます。\n\n```shellsession\n# export N_NODE_MIRROR=https://github.com/wekan/node-v14-esm/releases/download\n# n 14.21.4\n# node -v\nv14.21.4\n# cd /opt/wekan/.build/bundle/programs/server\n# npm install\n```\n\n<br />\n\nゴミ掃除します。(続き)\n\n```shellsession\n# cd ../../../..\n# cd .build/bundle\n# find . -type d -name '*-garbage*' | xargs rm -rf\n# find . -name '*phantom*' | xargs rm -rf\n# find . -name '.*.swp' | xargs rm -f\n# find . -name '*.swp' | xargs rm -f\n```\n\n<br />\n\nここまでで、rebuild-wekan.sh の内容は終了です。  \n\n<br />\n\n起動してみます。\n\n```shellsession\n# cd /opt/wekan\n# ./start-wekan.sh\n/opt/wekan/.build/bundle/programs/server/npm/node_modules/meteor/promise/node_modules/meteor-promise/promise_server.js:218\n      throw error;\n      ^\n```\n\n<br />\n\nエラーになりました。それもそのはず、mongodbをインストールしていませんでした。\n\n<br />\n\n# mongodbインストール\n  \nmongodbのyumリポジトリ情報を追加します。\n\n```shellsession\n# vi /etc/yum.repos.d/mongodb-org.repo\n```\n\n```shellsession\n[mongodb-org-4.4]\nname=MongoDB Repository\nbaseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/4.4/x86_64/\ngpgcheck=1\nenabled=1\ngpgkey=https://www.mongodb.org/static/pgp/server-4.4.asc\n```\n\n<br />\n\nmongodb(クライアント、サーバー)4.4をインストールします。\n\n```shellsession\n# yum install -y mongodb-org\n# mongo -version\nMongoDB shell version v4.4.5\n```\n\n<br />\n\nmongodbを起動します。\n\n```shellsession\n# systemctl enable mongod\n# systemctl start mongod\n```\n\n<br />\n\nもう一度、start-wekan.shでWekanを起動してみます。\n\n```shellsession\n# ./start-wekan.sh\n{\"line\":\"87\",\"file\":\"percolate_synced-cron.js\",\"message\":\"SyncedCron: Scheduled \\\"notification_cleanup\\\" next run @Sat Nov 11 2023 00:57:02 GMT-0800 (Pacific Standard Time)\",\"time\":{\"$date\":1699693022952},\"level\":\"info\"}\n```\n\n起動しました!  \n<br />\nCTRL + C で停止して、systemdに登録します。  \n\n<br />\n\n# wekan起動設定\n  \nwekan起動時の環境変数を記述します。\n\n```shellsession\n# vi /opt/wekan/.build/bundle/.env\n```\n\n```shellsession\nMONGO_URL='mongodb://127.0.0.1:27017/wekan'\nROOT_URL='http://wekan.itccorporation.jp'\nMAIL_URL='smtp://user:pass@mailserver.example.com:587'\nMAIL_FROM='wekan@itccorporation.jp'\nPORT=3001\n```\n\n<br />\n\nsystemdに登録します。\n\n```shellsession\n# vi /etc/systemd/system/wekan.service\n```\n\n```shellsession\nMONGO_URL='mongodb://127.0.0.1:27017/wekan'\n[Unit]\nDescription=Wekan Server\nAfter=syslog.target\nAfter=network.target\n\n[Service]\nType=simple\nRestart=on-failure\nStartLimitInterval=86400\nStartLimitBurst=5\nRestartSec=10\nExecStart=/usr/local/bin/node /opt/wekan/.build/bundle/main.js\nEnvironmentFile=/opt/wekan/.build/bundle/.env\nExecReload=/bin/kill -USR1 $MAINPID\nRestartSec=10\nWorkingDirectory=/opt/wekan\nStandardOutput=syslog\nStandardError=syslog\nSyslogIdentifier=Wekan\n\n[Install]\nWantedBy=multi-user.target\n```\n\n<blockquote class=\"info\">\n<p>root権限起動のため、</p>\n<pre><code>User=wekan\nGroup=wekan</pre></code>\n<p>のような記述は省略しています。</p>\n<p>インターネットへの疎通が無い社内の閉じたLAN限定で運用するため、root起動ですが、通常は、ユーザー権限でセットアップ、起動した方が良いと思います。</p>\n</blockquote>\n\n<br />\n\n```shellsession\n# systemctl daemon-reload\n# systemctl enable wekan\n# systemctl start wekan\n```\n\nここで起動しませんでした。\n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;\"><strong>【2023年11月更新】</strong></span></p>\n<p>ブログ投稿当時の手順では、問題なく起動していました。</p>\n</blockquote>\n\n```shellsession\n# journalctl -u wekan.service\nNov 11 00:58:32 localhost.localdomain systemd[1]: Started Wekan Server.\nNov 11 00:58:35 localhost.localdomain Wekan[8000]: /opt/wekan/.build/bundle/programs/server/node_modules/fibers/future.js:280\nNov 11 00:58:35 localhost.localdomain Wekan[8000]:                                                 throw(ex);\nNov 11 00:58:35 localhost.localdomain Wekan[8000]:                                                 ^\nNov 11 00:58:35 localhost.localdomain Wekan[8000]: TypeError [ERR_INVALID_ARG_TYPE] [ERR_INVALID_ARG_TYPE]: The \"path\" argument must be of type string. Received undefined\nNov 11 00:58:35 localhost.localdomain Wekan[8000]:     at new NodeError (internal/errors.js:322:7)\nNov 11 00:58:35 localhost.localdomain Wekan[8000]:     at validateString (internal/validators.js:124:11)\nNov 11 00:58:35 localhost.localdomain Wekan[8000]:     at Object.join (path.js:1148:7)\nNov 11 00:58:35 localhost.localdomain Wekan[8000]:     at module (models/attachments.js:41:22)\nNov 11 00:58:35 localhost.localdomain Wekan[8000]:     at fileEvaluate (packages/modules-runtime.js:336:7)\nNov 11 00:58:35 localhost.localdomain Wekan[8000]:     at Module.require (packages/modules-runtime.js:238:14)\nNov 11 00:58:35 localhost.localdomain Wekan[8000]:     at Module.moduleLink [as link] (/opt/wekan/.build/bundle/programs/server/npm/node_modules/meteor/modules/node_modules/@meteorjs/reify/lib>\nNov 11 00:58:35 localhost.localdomain Wekan[8000]:     at module (server/publications/attachments.js:1:24)\nNov 11 00:58:35 localhost.localdomain Wekan[8000]:     at fileEvaluate (packages/modules-runtime.js:336:7)\nNov 11 00:58:35 localhost.localdomain Wekan[8000]:     at Module.require (packages/modules-runtime.js:238:14)\nNov 11 00:58:35 localhost.localdomain Wekan[8000]:     at require (packages/modules-runtime.js:258:21)\nNov 11 00:58:35 localhost.localdomain Wekan[8000]:     at /opt/wekan/.build/bundle/programs/server/app/app.js:189382:1\nNov 11 00:58:35 localhost.localdomain Wekan[8000]:     at /opt/wekan/.build/bundle/programs/server/boot.js:385:38\nNov 11 00:58:35 localhost.localdomain Wekan[8000]:     at Array.forEach (<anonymous>)\nNov 11 00:58:35 localhost.localdomain Wekan[8000]:     at /opt/wekan/.build/bundle/programs/server/boot.js:210:21\nNov 11 00:58:35 localhost.localdomain Wekan[8000]:     at /opt/wekan/.build/bundle/programs/server/boot.js:439:7\nNov 11 00:58:35 localhost.localdomain Wekan[8000]:     at Function.run (/opt/wekan/.build/bundle/programs/server/profile.js:256:14)\nNov 11 00:58:35 localhost.localdomain Wekan[8000]:     at /opt/wekan/.build/bundle/programs/server/boot.js:438:13 {\nNov 11 00:58:35 localhost.localdomain Wekan[8000]:   code: 'ERR_INVALID_ARG_TYPE'\nNov 11 00:58:35 localhost.localdomain Wekan[8000]: }\n```\n\n<br />\n\n`WRITABLE_PATH=/data` 環境変数と /data ディレクトリが必要になったようです。  \n(参考:`https://github.com/wekan/charts/issues/14`)\n\n```shellsession\n# mkdir /data\n# vi /opt/wekan/.build/bundle/.env\nWRITABLE_PATH=/data←追加\n# reboot\n```\n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;\"><strong>【2023年11月更新】</strong></span></p>\n<p><code>wekan.service: Start request repeated too quickly.</code></p>\n<p><code>wekan.service: Failed with result 'exit-code'.</code></p>\n<p><code>Failed to start Wekan Server.</code></p>\n<p>とエラーになり、<code>systemctl start wekan</code> で起動せず、なぜかリブートが必要でした。</p>\n</blockquote>\n\n<br />\n\n# ブラウザアクセス\n  \nファイアウォール有りで、外から見る場合、3001ポートを開ける必要があります。\n\n```shellsession\n# firewall-cmd --zone=public --add-port=3001/tcp --permanent\n# firewall-cmd --reload\n```\n\n<br />\n\n`http://wekan.itccorporation.jp:3001` へブラウザでアクセス  \n\n<br />\n\n<a href=\"https://itc-engineering-blog.imgix.net/wekan-install/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://itc-engineering-blog.imgix.net/wekan-install/image1.png\" alt=\"ブラウザでアクセス\" width=\"944\" height=\"711\" loading=\"lazy\"></a>\n\n<br />\n\nできました!\n","description":"Wekan Open-Source kanban をソースコードを使ってインストールしてみました。 Wekanは、OSSのかんばん管理ツールです。社内タスクの管理、見える化に役立ちます。 ソースコードは、Node.js、Meteorフレームワークで構成されています。MITライセンスです。 同じ目的の場合、Trelloが有名ですが、閉じた環境で自力運用したくて、Wekanを選びました。 インストール環境:CentOS Linux release 8.3.2011 (VMware上、インターネット接続あり) 別記事「CentOS8にwekanをインストール(snap編)」では、snapを使い、インストールしましたが、今回は、snapを使わず、dockerでもなく、ソースからインストールしていきます。 github.com/wekan から rebuild-wekan.sh というのを見つけて、この内容を行えばできそうと思い、やってみました。 基本的に rebuild-wekan.sh の内容通りでしたが、今回の環境の場合、CentOSのため、rebuild-wekan.sh の序盤 apt-get でいきなりコケますので、dnf, yumで代替して進めています。","reflect_updatedAt":true,"reflect_revisedAt":true,"seo_images":[{"id":"0nsp706a6x","createdAt":"2021-07-16T10:06:18.364Z","updatedAt":"2023-11-11T11:06:24.979Z","publishedAt":"2021-07-16T10:06:18.364Z","revisedAt":"2023-11-11T11:06:24.979Z","url":"https://itc-engineering-blog.imgix.net/wekan-install/ITC_Engineering_Blog2.png","alt":"CentOS8にwekanをインストール(ソースからビルド編)","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":"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**1. `mysql -u root -p`**\n\n<div style=\"padding-left: 1em;\">MariaDB サーバーに root ユーザーとしてログインします。<br /><code>-p</code> オプションはパスワードを求めるためのものです。</div>\n\n**2. `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**3. `GRANT ALL PRIVILEGES ON ejbca.* TO 'ejbca'@'localhost';`**\n\n<div style=\"padding-left: 1em;\">新しく作成した <code>ejbca</code> ユーザーに、<code>ejbca</code> データベースの全てのテーブルに対する全権限を付与します。</div>\n\n**4.`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**1. `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**2. `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**1. `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**2. `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**1. `jboss-cli.sh --connect`**\n\n<div style=\"padding-left: 1em;\">WildFly の管理 CLI に接続します。</div>\n\n**2. `/subsystem=elytron/credential-store=defaultCS:add(...)`**\n\n<div style=\"padding-left: 1em;\">Elytron サブシステム内で <code>defaultCS</code> という名前の認証情報ストアを作成します。</div>\n\n**3. `path=keystore/credentials, relative-to=jboss.server.config.dir`**\n\n<div style=\"padding-left: 1em;\">認証情報ストアの物理的な場所を指定します。これは、WildFly の設定ディレクトリ内の <code>keystore/credentials</code> というパスになります。</div>\n\n**4. `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**5. `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**1. `/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**2. `/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**3. `/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**1. `/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**2. `/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**3. `/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**1. `/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**2. `/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**3. `/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**4. `/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**5. `/opt/wildfly/bin/jboss-cli.sh --connect ':reload'`**\n\n<div style=\"padding-left: 1em;\">WildFly サーバーをリロードします。これにより、上記の設定変更が反映されます。</div>\n\n**6. `/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**1. `/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**2. `/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**3. `/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**4. `/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**5. `/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**6. `/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**1. `/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**2. `/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**3. `/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**4. `/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**5. `/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**6. `/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**7. `/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**8. `/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**1. `/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**2. `/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**3. `/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**1. `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**2. `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":[]},{"id":"ydznz8j2o","createdAt":"2021-07-10T09:21:04.197Z","updatedAt":"2021-07-16T10:21:18.970Z","publishedAt":"2021-07-10T09:21:04.197Z","revisedAt":"2021-07-16T10:21:18.970Z","title":"StrapiとNext.jsを使って静的ウェブサイトを構築","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":"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":"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":"yd-ufoing","createdAt":"2021-06-03T13:52:07.359Z","updatedAt":"2021-08-31T12:04:05.454Z","publishedAt":"2021-06-03T13:52:07.359Z","revisedAt":"2021-08-31T12:04:05.454Z","topics":"Jamstack","logo":"/logos/Jamstack.png","needs_title":false}],"content":"# はじめに\n弊社のWebサイト(<a href=\"https://itccorporation.jp\" target=\"_blank\">https://itccorporation.jp</a>)は、Next.js、ヘッドレスCMSのStrapiを使って、生成した静的Webサイトです。\n※用語については、下にまとめて簡単に解説があります。\n\n図で示すと、以下のようになっています。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/kousei.svg)\n\n<a href=\"https://itccorporation.jp\" target=\"_blank\">https://itccorporation.jp</a>のソースコードは、GitHubにあります。(<a href=\"https://github.com/itc-lab/itccorporation\" target=\"_blank\">https://github.com/itc-lab/itccorporation</a>)\n今回は、このソースコードを使い、静的Webサイトを生成するところまでやっていきたいと思います。\n\n<blockquote class=\"alert\">\n<p><span style=\"color: red;\"><strong>Webサーバー(上の図で言うと一番右)のところの解説は省略します。</strong></span></p>\n</blockquote>\n\n<br />\n\n・[Node.jsインストール](#anchor1)  \n・[Strapiインストール](#anchor2)  \n・[Webサイトソースコードの配置](#anchor3)  \n・[Strapi初期設定](#anchor4)  \n・[Strapiでデータスキーマ作成](#anchor5)  \n・[Strapiでデータ登録](#anchor6)  \n・[GraphQL有効化](#anchor7)  \n・[Webサーバーへデプロイ](#anchor8)  \n\n<br />\n\n<blockquote class=\"info\">\n<p><strong>【 Next.js 】</strong></p>\n<p>「Next.js」とは、Vercel社が開発したReactのフレームワークです。</p>\n<p>Static Site Generator(SSG)・静的サイトジェネレーターとしての役割を果たします。</p>\n</blockquote>\n\n<blockquote class=\"alert\">\n<p>Next.jsのnext/image(Imageコンポーネント)による画像最適化を使う場合、VercelにデプロイするかCloudinaryなどの外部CDNを使う必要があります。※2021/07現在</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p><strong>【 React 】</strong></p>\n<p>「React」とは、Facebook社が開発したJavaScriptのフレームワークです。</p>\n<p>コンポーネントと呼ばれる部品(UI・画面の一部分)ごとに開発ができます。画面更新の際、差分を検出して差分だけ更新するため、動作が速いという特徴があります。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p><strong>【 静的Webサイト 】</strong></p>\n<p>決まった内容を表示するだけのサイトです。ユーザーによって画面が切り替わったり、入力内容によって切り替わったり、サーバー側に何らかの処理が必要な場合は動的Webサイトになります。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p><strong>【 ヘッドレスCMS 】</strong></p>\n<p>まず、「CMS」とは、WordPressのように全体の枠(決まったデザイン)ができあがっていて、その中に記事を書き込みできるなどのコンテンツの管理システムの事です。「ヘッドレスCMS」とは、全体の枠(決まったデザイン)が無く、記事の内容(コンテンツ)だけを登録し、APIによって取り出されるというものです。ヘッドレスCMSの場合、その都度デザインを作らないといけませんが、反面、自由です。ヘッドレスCMS提供サービスは、Contentful、microCMS、Strapi、などいろいろあります。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p><strong>【 Strapi 】</strong></p>\n<p>ヘッドレスCMSの一つですが、クラウドのWebサービスのみならず、ソースコードが公開されているため、それを利用し、自分が管理しているサーバーにヘッドレスCMS、APIを構築できます。</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>node v14.17.2</code></p>\n<p>になります。</p>\n</blockquote>\n\n<blockquote class=\"alert\">\n<p>ソースコードの解説は行いません。</p>\n<p>Google reCAPTCHAは、設定済みで、キーは取得済みとします。</p>\n</blockquote>\n\n<br />\n\n<a class=\"anchor\" id=\"anchor1\"></a>\n\n# Node.jsインストール\n\n<blockquote class=\"warn\">\n<p>root権限で作業していますので、全てsudoは省略しています。</p>\n</blockquote>\n\nパッケージを最新にします。※必須ではありません。\n\n```sh\n# apt update\n# apt upgrade\nDo you want to continue? [Y/n]Y\n```\n※以降基本的にYのため、-yを付けます。  \n -y は、? [Y/n]: のようなときに自動的に y とするオプションです。\n\n<br />\n\ncurlをインストールします。\n\n```sh\n# apt install -y curl\n```\n\n<br />\n\nnode 14.x をインストールします。\n\n```sh\n# curl -sL https://deb.nodesource.com/setup_14.x -o nodesource_setup.sh\n# bash nodesource_setup.sh\n# apt install -y nodejs\n# node -v\nv14.17.2\n```\n\n<blockquote class=\"info\">\n<p><code>node</code>のバージョンはインストール時期によって異なります。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>Node.jsのパッケージ管理システム<code>npm</code>も同時にインストールされます。</p>\n</blockquote>\n\n<br />\n\n<a class=\"anchor\" id=\"anchor2\"></a>\n\n# Strapiインストール\n\n空の状態のStrapiをインストールします。  \n今回、`/home/admin/mycorporation_backend`にインストールします。\n\n```sh\n# cd /home/admin\n# npx create-strapi-app mycorporation_backend --quickstart\n→そのまま起動してくるため、CTRL+Cで停止\n```\n\n<blockquote class=\"info\">\n<p><strong>【 npx 】</strong></p>\n<p><code>npm</code>で管理されたプログラムの一つです。<code>npx</code>を使うと、オンメモリで依存関係を解決した状態で、引数に指定したプログラムを実行できます。ディスクを汚さず、一度きりしか実行しないプログラムを実行するのに向いています。</p>\n</blockquote>\n\n<br />\n\n<a class=\"anchor\" id=\"anchor3\"></a>\n\n# Webサイトソースコードの配置\n## ソースコード展開\n<a href=\"https://github.com/itc-lab/itccorporation\" target=\"_blank\">https://github.com/itc-lab/itccorporation</a> からソースコードをダウンロードして、独自プロジェクトとします。  \nCode → Download ZIP で itccorporation-master.zip をダウンロードします。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image1.png)\n\n<br />\n\nソースコードを展開し、ディレクトリをリネームします。  \n※ここでは、`/home/admin/mycorporation_frontend`とします。\n\n```sh\n# cd /home/admin\n# unzip itccorporation-master.zip\n# mv itccorporation-master mycorporation_frontend\n# cd mycorporation_frontend\n```\n\n<br />\n\n## 環境変数変更\n環境変数を自分の環境に合わせます。\n\n```sh\n# cp .env.local.example .env.local\n# vi .env.local\nGRAPHQL_API=http://localhost:1337/graphql\nNEXT_PUBLIC_RECAPTCHA_SITE_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n```\n\n<br />\n\n**GRAPHQL_API**  \n今回、1337ポートで動くStrapiの機能を利用するため、`http://localhost:1337/graphql` のままにします。\n\n<br />\n\n**NEXT_PUBLIC_RECAPTCHA_SITE_KEY**  \nGoogle reCAPTCHAの「サイトキーをコピーする」に表示される文字列です。\n\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image2.png\" alt=\"\" width=\"600px\">\n\n<br />\n\n## 環境変数変更\n以下の図のようにシンボリックリンクを作成します。  \n`uploads`ディレクトリは、Strapiからアップロードした画像ファイルが格納されるディレクトリになります。\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/symlink.svg)\n\n```sh\n# ln -s /home/admin/mycorporation_backend/public/uploads public/uploads\n```\n\n<br />\n\n<a class=\"anchor\" id=\"anchor4\"></a>\n\n# Strapi初期設定\n\nStrapiを起動します。\n\n```sh\n# cd /home/admin/mycorporation_backend\n# npm run develop\n```\n\n何も設定変更していない時は、1337ポートで起動します。  \nブラウザから `http://[Strapiを起動したサーバーのIPアドレス]:1337` にて、アクセスします。\n\n「Create the first administrator」をクリックします。\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image3.png\" alt=\"\" width=\"600px\">\n<br />\n「First name」「Last name」「Email」「Password」「Confirmation Password」を入力し、「LET'S START」をクリックします。\n※「Email」は、ログインIDに使われます。メールの設定をしない場合、メールは送信されません。\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image4.png\" alt=\"\" width=\"600px\">\n<br />\n\n<br />\n\n<a class=\"anchor\" id=\"anchor5\"></a>\n\n# Strapiでデータスキーマ作成\nStrapiでスキーマ(空のテーブル)を作成していきます。  \n<br />\n「Content-Types Builder」をクリックします。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image5.png\" alt=\"\" width=\"600px\">\n<br />\n「+Create new collection type」をクリックします。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image6.png\" alt=\"\" width=\"600px\">\n<br />\n「Display name」のところに \"Available\" と入力し、「Continue」ボタンをクリックします。  \n→DBで言うAvailableテーブルを作成するイメージです。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image7.png\" alt=\"\" width=\"600px\">\n<br />\n「Text」をクリックします。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image8.png\" alt=\"\" width=\"600px\">\n<br />\n「Name」のところに \"name\" と入力し、「Short text」にチェックを入れて、「+ Add another field」ボタンをクリックします。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image9.png\" alt=\"\" width=\"600px\">\n<br />\n「Media」をクリックします。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image10.png\" alt=\"\" width=\"600px\">\n<br />\n「Name」のところに \"logo\" と入力し、「Single media」にチェックを入れて、「+ Add another field」ボタンをクリックします。  \n→単一画像を登録できるフィールドになります。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image11.png\" alt=\"\" width=\"600px\">\n<br />\n・・・以降、下記の[スキーマ](#schema)の通りに作成します。  \n(Textは全て「Short text」、Mediaは全て「Single media」です。)  \n<br />\nRelationのところは、以下のように、左側に \"seo_Images\" を入力して、右矢印が複数のアイコンを選択、右側で Seo-Image を選択します。  \n→これにより、OpenGraphテーブルのseo_imagesフィールドは、Seo-Imagesテーブルの複数のデータに紐づけが可能になります。\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image12.png\" alt=\"\" width=\"600px\">\n<br />\nTwitterは、「SINGLE TYPE」→「Create new single type」で作成します。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image13.png\" alt=\"\" width=\"600px\">\n<br />\n\n<a class=\"anchor\" id=\"schema\"></a>\n\n<strong>スキーマ</strong>  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image14.png\" alt=\"\" width=\"300px\">\n\n<br />\n\n<a class=\"anchor\" id=\"anchor6\"></a>\n\n# Strapiでデータ登録\nデータを登録していきます。  \n<br />\n「Availales」をクリックし、「Add New Availables」をクリックします。※作ったのは「Available」テーブルですが、自動的に複数形表示になります。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image40.png\" alt=\"\" width=\"600px\">\n<br />\n「Name」:Strapi  \n「Description」:ストラピ  \n「Priority」:1  \n「Logo」:Strapiのロゴ画像をドラッグ&ドロップ\nとし、「Save」ボタンをクリックします。\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image15.png\" alt=\"\" width=\"600px\">\n<br />\n「Publish」ボタンをクリックします。  \n→「Publish」ボタンを押さないと有効化されませんので、毎回忘れずに押します。\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image16.png\" alt=\"\" width=\"600px\">\n<br />\n他、以下のように登録していきます。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image17.png\" alt=\"\" width=\"500px\">  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image18.png\" alt=\"\" width=\"500px\">  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image19.png\" alt=\"\" width=\"500px\">  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image20.png\" alt=\"\" width=\"500px\">  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image21.png\" alt=\"\" width=\"500px\">  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image22.png\" alt=\"\" width=\"500px\">  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image23.png\" alt=\"\" width=\"500px\">  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image24.png\" alt=\"\" width=\"500px\">  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image25.png\" alt=\"\" width=\"500px\">  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image26.png\" alt=\"\" width=\"500px\">  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image27.png\" alt=\"\" width=\"500px\">  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image28.png\" alt=\"\" width=\"500px\">  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image29.png\" alt=\"\" width=\"500px\">  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image30.png\" alt=\"\" width=\"500px\">  \nOpenGraphsの「Seo_images」のところですが、Seo-imageテーブルのデータと紐づけるため、選択します。(複数選択可能です。)  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image31.png\" alt=\"\" width=\"300px\">\n<br />\n\n<br />\n\n<a class=\"anchor\" id=\"anchor7\"></a>\n\n# GraphQL有効化\n## GraphQLインストール\n<blockquote class=\"info\">\n<p><strong>【 GraphQL 】</strong></p>\n<p>「GraphQL」とは、Facebookにより開発されたAPI用のクエリ言語です。似たものとして、「REST」がありますが、「GraphQL」の方は、\"全部\"では無く、\"これとこれとこれ\"という感じで、あらかじめ対象データを指定して、命令を出せるという特徴があります。</p>\n</blockquote>\n\n「Marketplace」をクリックし、「GRAPHQL」のところの「Download」ボタンをクリックします。  \n→しばらくすると、StrapiにGraphQL機能が組み込まれます。\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image32.png\" alt=\"\" width=\"600px\">\n<br />\n\nhttp://[StrapiのサーバーIPアドレス]/graphql\nにアクセスすると、クエリを発行してテストすることができます。\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image33.png\" alt=\"\" width=\"600px\">\n<br />\n→この時点では、「Forbidden」が返り、うまくいきません。Strapi側のアクセス権の設定が必要です。\n\n<br />\n\n## アクセス権の設定\n「Settings」→「Roles」→「Public」をクリックします。\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image34.png\" alt=\"\" width=\"600px\">\n<br />\n「find」、「findone」チェックボックス全てにチェックを入れて、「Save」ボタンをクリックします。\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image35.png\" alt=\"\" width=\"600px\">\n<br />\n再び、`http://[StrapiのサーバーIPアドレス]/graphql` にアクセスし、クエリを実行すると、データが取り出せることが確認できます。  \n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image39.png\" alt=\"\" width=\"600px\">\n<br />\n\n<br />\n\n## 動作確認\n\n<blockquote class=\"warn\">\n<p>Strapiが起動(<code>npm run develop</code>)したままで下記の実施が必要です。起動していない場合、データの取り出しに失敗します。</p>\n</blockquote>\n\nWebサイトのプログラムの方へ戻って、`npm install`を実行します。  \n→これにより、`package.json`に書かれた必要なもの全てが自動的にインストールされます。\n\n```sh\n# cd /home/admin/mycorporation_frontend\n# npm install\n```\n\nインストールが終わったら、起動します。\n\n```sh\n# npm run dev\n```\n\n<br />\n\n`http://[IPアドレス]:3000/` でアクセスします。\n\n<br />\n\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image36.png\" alt=\"\" width=\"500px\">  \n<br />\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image37.png\" alt=\"\" width=\"500px\">  \n<br />\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image38.png\" alt=\"\" width=\"500px\">  \n\n<br />\n\n表示されました!\n\n<br />\n\n<a class=\"anchor\" id=\"anchor8\"></a>\n\n# Webサーバーへデプロイ\n\nWebサーバーへデプロイは簡単です。\n\n```sh\n# npm run build\n```\n\nを実行すると、カレントディレクトリに out ディレクトリができますので、これをドキュメントルートへ移動するだけになります。  \n→例えば、`mv out /var/www/html` のようにWebサーバーが参照する場所に置くだけになります。  \n\n<br />\n\n![](https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/kousei.svg)\n\n<br />\n\n`https://[WebサイトのFQDN]/` でアクセスします。\n\n<br />\n\n<img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/image36.png\" alt=\"\" width=\"500px\">  \n\n<br />\n\nできました!\n","description":"弊社のWebサイト(https://itccorporation.jp)は、Next.js、ヘッドレスCMSのStrapiを使って、生成した静的Webサイトです。 ※用語については、下にまとめて簡単に解説があります。 図で示すと、以下のようになっています。 https://itccorporation.jpのソースコードは、GitHubにあります。(https://github.com/itc-lab/itccorporation) 今回は、このソースコードを使い、静的Webサイトを生成するところまでやっていきたいと思います。 Webサーバー(上の図で言うと一番右)のところの解説は省略します。 ・Node.jsインストール ・Strapiインストール ・Webサイトソースコードの配置 ・Strapi初期設定 ・Strapiでデータスキーマ作成 ・Strapiでデータ登録 ・GraphQL有効化 ・Webサーバーへデプロイ","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"q98_g0d484","createdAt":"2021-07-16T10:01:07.176Z","updatedAt":"2021-07-16T10:01:07.176Z","publishedAt":"2021-07-16T10:01:07.176Z","revisedAt":"2021-07-16T10:01:07.176Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/corporatesite/ITC_Engineering_Blog.png","alt":"StrapiとNext.jsを使って静的ウェブサイトを構築","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 の穴の位置が同じになり、スペーサーを4つ差し込んで取り付けます。\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=\"スペーサーを4つ差し込んで取り付け1\" 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=\"スペーサーを4つ差し込んで取り付け2\" 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\n4本の大きいネジを下から挿して、ナットで止めれば完成です。\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=\"上下1\" 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=\"上下2\" width=\"521\" height=\"521\" loading=\"lazy\"></a>\n\n<br />\n\n# OSイメージ書き込み\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>作業PCを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 カードを作業PCに認識させて</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=\"日本語を選択1\" 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=\"日本語を選択2\" 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=\"ログイン画面1\" 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=\"ログイン画面2\" 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":"dapr-local-dev","createdAt":"2022-12-06T13:54:41.073Z","updatedAt":"2022-12-06T13:54:41.073Z","publishedAt":"2022-12-06T13:54:41.073Z","revisedAt":"2022-12-06T13:54:41.073Z","title":"Node.js,Python,ReactでDaprの状態管理アプリを作成してローカル環境で動作確認","category":{"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},"topics":[{"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":"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\nDapr のサービス呼び出しと状態管理を利用する簡易 Web アプリを作成しました。何もない状態からスタートで記事にしたいと思います。  \n今回は、Dapr の Service-to-service invocation(サービス呼び出し) と State management(状態管理) を使います。  \nDapr CLI を使って、動作確認するところまで実施します。  \nなお、続きの記事も同時に公開していて、そちらは、この記事のアプリを Bicep を使って、Azure Container Apps + Dapr 環境 にデプロイします。  \n次回記事:「<a href=\"https://itc-engineering-blog.netlify.app/blogs/azure-aca-dapr-bicep\" target=\"_blank\">Bicepを使ってAzure Container AppsとDaprのマイクロサービスをデプロイ</a>」  \n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/zu1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/zu1.png\" alt=\"Dapr 概要図\" width=\"938\" height=\"435\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<blockquote class=\"alert\">\n<p>注意:今回も、次回も、記事中に Kubernetes 絡みの話は出てきません。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 Dapr(ダパァ/ダッパー) 】</p>\n<p>Dapr は、クラウドネイティブおよびサーバーレスコンピューティングをサポートするように設計された無料のオープンソースランタイムシステムです。</p>\n<p>Dapr は、いろいろなビルディングブロック(機能)を有し、HTTP API/gRPC API で提供します。(一つ一つの説明は省略します。)</p>\n<p>API は、様々な言語で呼び出すことができて、クラウドインフラ、エッジインフラ、オンプレミス環境で利用可能です。</p>\n<p>Dapr は、コンテナーの個別のプロセスとして、サイドカー アーキテクチャでアプリケーションと共に実行されます。</p>\n<p>どの環境でも(今回利用する State management の場合、どの DB を使っても。)アプリは、同一の実装になります。必要なのは、Dapr コンポーネントの差し替えになります。</p>\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/zu2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/zu2.png\" alt=\"Dapr(ダパァ/ダッパー)\" width=\"1224\" height=\"602\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 ビルディングブロック 】</p>\n<p>ビルディング ブロックは、コードから呼び出すことができ、1 つ以上の Dapr コンポーネントを使用する HTTP または gRPC API です。</p>\n<p>Dapr のコンポーネントは、Dapr の各ビルディング ブロック機能の具体的な実装を提供します。</p>\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/zu3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/zu3.png\" alt=\"ビルディングブロック\" width=\"1120\" height=\"546\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 サイドカーパターン 】</p>\n<p>サイドカーパターンは、メインのコンテナと、補助的な機能を提供するコンテナで構成されます。</p>\n<p>このパターンは、オートバイに取り付けられるサイドカーに似ているため、\"サイドカー\" と名付けられています。</p>\n<p>このパターンでは、サイドカーは親アプリケーションに接続され、アプリケーションにサポート機能を提供します。</p>\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/zu4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/zu4.png\" alt=\"サイドカーパターン\" width=\"847\" height=\"258\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 Service-to-service invocation サービス呼び出し 】</p>\n<p>サービス呼び出しを使用すると、アプリケーションは、標準の gRPC または HTTP プロトコルを使用して、他のアプリケーションと確実かつ安全に通信できます。</p>\n<p>サービスディスカバリ(IP アドレス:ポートを見つけたり、ドメイン名を DNS に登録したり)の心配は有りません。サービス名で連携先サービスを呼び出せます。</p>\n<p></p>\n<p><strong>動作概要:</strong></p>\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/zu5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/zu5.png\" alt=\"Service-to-service invocation サービス呼び出し\" width=\"1144\" height=\"509\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n<p><strong>1.</strong> サービス A は、サービス B をターゲットとする HTTP または gRPC 呼び出しを行います。呼び出しは、ローカルの Dapr サイドカーに送られます。</p>\n<p><strong>2.</strong> Dapr は、指定されたホスティング プラットフォームで実行されている名前解決コンポーネントを使用して、サービス B の場所を検出します。</p>\n<p><strong>3.</strong> Dapr がメッセージをサービス B の Dapr サイドカーに転送します。</p>\n<p> 注: Dapr サイドカー間のすべての呼び出しは、パフォーマンスのために gRPC を経由します。</p>\n<p> サービスと Dapr サイドカー間の呼び出しのみ、HTTP または gRPC のいずれかにすることができます。</p>\n<p><strong>4.</strong> サービス B の Dapr サイドカーは、サービス B の指定されたエンドポイント (またはメソッド) にリクエストを転送します。次に、サービス B はそのビジネス ロジック コードを実行します。</p>\n<p><strong>5.</strong> サービス B はサービス A に応答を送信します。応答はサービス B のサイドカーに送信されます。</p>\n<p><strong>6.</strong> Dapr は、サービス A の Dapr サイドカーに応答を転送します。</p>\n<p><strong>7.</strong> サービス A が応答を受信します。</p>\n</blockquote>\n\n<blockquote class=\"info\">\n<p>【 State management 状態管理 】</p>\n<p>Dapr は、キー/値ベースの状態およびクエリ API を提供します。</p>\n<p>サポートされているストア(バックエンドの DB)は、たくさんあります。</p>\n<p><a href=\"https://docs.dapr.io/reference/components-reference/supported-state-stores/\" target=\"_blank\">こちら</a>(https://docs.dapr.io/reference/components-reference/supported-state-stores/)にリストがありますが、今回、この中から、Redis を使います。</p>\n<p><a href=\"https://itc-engineering-blog.netlify.app/blogs/azure-aca-dapr-bicep\" target=\"_blank\">次回の記事</a>では、Azure Cosmos DB を使います。</p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>作業を始める前に、docker, python, node インストール済みとします。</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 v14.20.0</code></p>\n<p> <code>Python 3.8.10</code></p>\n<p> <code>Docker 20.10.21</code></p>\n<p> <code>Dapr CLI 1.9.1</code></p>\n<p> <code>Dapr Runtime 1.9.5</code></p>\n<p> <code>React 18.2.0</code></p>\n</blockquote>\n\n<br />\n\n# 概要\n\nnode-service - dapr → dapr - python-service  \nと python に実装されている /order API を呼び出して、データ登録、取得、削除を行います。  \nデータはハードコーディングされていて、1種類だけとします。  \n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/zu6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/zu6.png\" alt=\"Dapr利用の今回作成するアプリ概要図\" width=\"1343\" height=\"521\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<blockquote class=\"info\">\n<p><a href=\"https://github.com/Azure-Samples/container-apps-store-api-microservice\" target=\"_blank\">こちら</a>(https://github.com/Azure-Samples/container-apps-store-api-microservice)のソースコードを参考にしていて、大部分同じ記述があります。</p>\n</blockquote>\n\n<br />\n\n画面は、React です。ビルドして使います。node-service で `/` など、API 以外のアクセスがあった時、ビルド済みの index.html を読み取って返しています。  \nReact である必然性はかなり少なく、index.html とか手で書いて置いても良かったかなとは思います。\n\n<br />\n\n# ソースコード準備\n\n<blockquote class=\"info\">\n<p>作成済みの全体ソースコードは、GitHub リポジトリ <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\ncreate-react-app で画面作成(`node-service/client/`)  \n↓  \nホスティング/ルーティングのための node プログラム作成(`node-service/index.js`)  \n↓  \norder API サービスの python プログラム作成(`python-service/`)  \nと準備していきます。  \n\n<br />\n\n## create-react-app\n\n```shellsession\n$ mkdir -p hello-dapr-app/node-service\n$ cd hello-dapr-app/node-service\n$ npx create-react-app client --template typescript\n```\n\nclient ディレクトリ配下に:3000 で起動する React アプリ一式が作成されます。\nこれの `node-service/client/src/App.tsx` を以下の内容に書き換えます。\n\n```tsx:node-service/client/src/App.tsx\nimport React, { useState } from \"react\";\nimport \"./App.css\";\n\nfunction App() {\n  const my_api_url =\n    process.env.REACT_APP_MY_API_URL || \"http://localhost:3000\";\n  const [message, setMessage] = useState(\"\");\n  const postOrder = async () => {\n    try {\n      const res = await fetch(`${my_api_url}/order`, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({ id: \"123\" }),\n      });\n      if (res.status === 200) {\n        const response = await res.text();\n        setMessage(response);\n      } else {\n        setMessage(\"Some error occured\");\n      }\n    } catch (err) {\n      console.log(err);\n    }\n  };\n  const getOrder = async () => {\n    try {\n      const res = await fetch(`${my_api_url}/order?id=123`, {\n        method: \"GET\",\n      });\n      if (res.status === 200) {\n        const response = await res.text();\n        setMessage(response);\n      } else {\n        setMessage(\"Some error occured\");\n      }\n    } catch (err) {\n      console.log(err);\n    }\n  };\n  const deleteOrder = async () => {\n    try {\n      const res = await fetch(`${my_api_url}/delete`, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({ id: \"123\" }),\n      });\n      if (res.status === 200) {\n        const response = await res.text();\n        setMessage(response);\n      } else {\n        setMessage(\"Some error occured\");\n      }\n    } catch (err) {\n      console.log(err);\n    }\n  };\n  return (\n    <div>\n      <button onClick={postOrder}>POST</button>\n      <button onClick={getOrder}>GET</button>\n      <button onClick={deleteOrder}>DELETE</button>\n      <div dangerouslySetInnerHTML={{ __html: message }}></div>\n    </div>\n  );\n}\n\nexport default App;\n```\n\n<br />\n\n```shellsession\n$ cd client\n$ npm start\n```\n\n結果、`http://localhost:3000` が以下のような画面になります。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/image1.png\" alt=\"localhost 3000 の画面\" width=\"655\" height=\"215\" loading=\"lazy\"></a>\n\n<br />\n\n最終的にビルドして、node.js → `client/*` をビルドした結果の html,js を参照させますので、CTRL + C で停止します。\n\n<br />\n\n## node.js\n\nnode.js + express + axios により、HTTP サーバー + Dapr へリクエストする役割のサービスを作成します。\n\n```shellsession\n$ cd node-service\n$ npm init\n(全部エンター)\n$ npm install axios\n$ npm install express\n```\n\n<br />\n\n`node-service/package.json` の `scripts` に 画面ビルド `buildclient`、サーバー起動 `start` を追加しておきます。\n\n```json:node-service/package.json\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\",\n    \"buildclient\": \"cd client && npm install && npm run build\",\n    \"start\": \"node index.js\"\n  },\n```\n\n<br />\n\n`node-service/index.js` を以下のように作成します。\n\n```tsx:node-service/index.js\nconst express = require(\"express\");\nconst path = require(\"path\");\nconst axios = require(\"axios\");\n\nconst app = express();\napp.use(express.json());\n\nconst port = 3000;\nconst pythonService = process.env.PYTHON_SERVICE_NAME || \"python-app\";\nconst daprPort = process.env.DAPR_HTTP_PORT || 3500;\n\n//use dapr http proxy (header) to call orders service with normal /order route URL in axios.get call\nconst daprSidecar = `http://localhost:${daprPort}`;\n\napp.get(\"/order\", async (req, res) => {\n  try {\n    var data = await axios.get(`${daprSidecar}/order?id=${req.query.id}`, {\n      headers: { \"dapr-app-id\": `${pythonService}` }, //sets app name for service discovery\n    });\n    res.setHeader(\"Content-Type\", \"application/json\");\n    res.send(\n      `<p>Order GET successfully!</p><br/><code>${JSON.stringify(\n        data.data\n      )}</code>`\n    );\n  } catch (err) {\n    res.send(\n      `<p>Error getting order<br/>Order microservice or dapr may not be running.<br/></p><br/><code>${err}</code>`\n    );\n  }\n});\n\napp.post(\"/order\", async (req, res) => {\n  try {\n    var order = req.body;\n    order[\"location\"] = \"Seattle\";\n    order[\"priority\"] = \"Standard\";\n    console.log(\n      \"Service invoke POST to: \" +\n        `${daprSidecar}/order?id=${req.query.id}` +\n        \", with data: \" +\n        JSON.stringify(order)\n    );\n    var data = await axios.post(\n      `${daprSidecar}/order?id=${req.query.id}`,\n      order,\n      {\n        headers: { \"dapr-app-id\": `${pythonService}` }, //sets app name for service discovery\n      }\n    );\n\n    res.send(\n      `<p>Order created!</p><br/><code>${JSON.stringify(data.data)}</code>`\n    );\n  } catch (err) {\n    res.send(\n      `<p>Error creating order<br/>Order microservice or dapr may not be running.<br/></p><br/><code>${err}</code>`\n    );\n  }\n});\n\napp.post(\"/delete\", async (req, res) => {\n  try {\n    var data = await axios.delete(`${daprSidecar}/order?id=${req.body.id}`, {\n      headers: { \"dapr-app-id\": `${pythonService}` },\n    });\n\n    res.setHeader(\"Content-Type\", \"application/json\");\n    res.send(`${JSON.stringify(data.data)}`);\n  } catch (err) {\n    res.send(\n      `<p>Error deleting order<br/>Order microservice or dapr may not be running.<br/></p><br/><code>${err}</code>`\n    );\n  }\n});\n\n// Serve static files\napp.use(express.static(path.join(__dirname, \"client/build\")));\n\n// For default home request route to React client\napp.get(\"/\", async function (_req, res) {\n  try {\n    return await res.sendFile(\n      path.join(__dirname, \"client/build\", \"index.html\")\n    );\n  } catch (err) {\n    console.log(err);\n  }\n});\n\napp.listen(process.env.PORT || port, () =>\n  console.log(`Listening on port ${port}!`)\n);\n```\n\n<br />\n\n起動するかどうか確認します。\n\n```shellsession\n$ npm start\n```\n\n<br />\n\n`Listening on port 3000!` と表示されたら、正常ですので、CTRL + C で止めます。\n\n<br />\n\n<blockquote class=\"info\">\n<p>Dapr に関係するキモとなる実装は、</p>\n<p><code>var data = await axios.get(`${daprSidecar}/order?id=${req.query.id}`, {</code></p>\n<p><code> headers: { \"dapr-app-id\": `${pythonService}` }, //sets app name for service discovery</code></p>\n<p><code>});</code></p>\n<p>のように、API(今回は、python の :5000)ではなく、Dapr にリクエストしているところです。</p>\n<p><code>dapr-app-id: &lt;Daprに伝えてあるアプリ名&gt;</code> ヘッダにより、リクエスト先が python だと分かります。</p>\n<p>したがって、python の IP アドレスやホスト名を知る必要はありません。</p>\n</blockquote>\n\n<br />\n\n## python\n\norder API サービス側の実装を python で行います。\n\n```shellsession\n$ cd ../\n(hello-dapr-app 直下)\n$ mkdir python-service\n```\n\n<br />\n\n`python-service/app.py` と 依存関係が書かれた `python-service/requirements.txt` を作成します。\n\n```python:python-service/app.py\nimport os\nimport logging\nimport flask\nfrom flask import request, jsonify\nfrom flask import json, abort\nfrom flask_cors import CORS\nfrom dapr.clients import DaprClient\n\nlogging.basicConfig(level=logging.INFO)\n\napp = flask.Flask(__name__)\nCORS(app)\n\n\n@app.route(\"/order\", methods=[\"GET\"])\ndef getOrder():\n    app.logger.info(\"order service called\")\n    with DaprClient() as d:\n        d.wait(5)\n        try:\n            id = request.args.get(\"id\")\n            if id:\n                # Get the order status from DB via Dapr\n                state = d.get_state(store_name=\"orders\", key=id)\n                if state.data:\n                    resp = jsonify(json.loads(state.data))\n                else:\n                    resp = jsonify(\"no order with that id found\")\n                resp.status_code = 200\n                return resp\n            else:\n                resp = jsonify('Order \"id\" not found in query string')\n                resp.status_code = 500\n                return resp\n        except Exception as e:\n            app.logger.info(e)\n            return str(e)\n        finally:\n            app.logger.info(\"completed order call\")\n\n\n@app.route(\"/order\", methods=[\"POST\"])\ndef createOrder():\n    app.logger.info(\"create order called\")\n    with DaprClient() as d:\n        d.wait(5)\n        try:\n            # Get ID from the request body\n            id = request.json[\"id\"]\n            if id:\n                # Save the order to DB via Dapr\n                d.save_state(\n                    store_name=\"orders\", key=id, value=json.dumps(request.json)\n                )\n                resp = jsonify(request.json)\n                resp.status_code = 200\n                return resp\n            else:\n                resp = jsonify('Order \"id\" not found in query string')\n                resp.status_code = 500\n                return resp\n        except Exception as e:\n            app.logger.info(e)\n            return str(e)\n        finally:\n            app.logger.info(\"created order\")\n\n\n@app.route(\"/order\", methods=[\"DELETE\"])\ndef deleteOrder():\n    app.logger.info(\"delete called in the order service\")\n    with DaprClient() as d:\n        d.wait(5)\n        id = request.args.get(\"id\")\n        if id:\n            # Delete the order status from DB via Dapr\n            try:\n                d.delete_state(store_name=\"orders\", key=id)\n                return f\"Item {id} successfully deleted\", 200\n            except Exception as e:\n                app.logger.info(e)\n                return abort(500)\n            finally:\n                app.logger.info(\"completed order delete\")\n        else:\n            resp = jsonify('Order \"id\" not found in query string')\n            resp.status_code = 400\n            return resp\n\n\napp.run(host=\"0.0.0.0\", port=os.getenv(\"PORT\", \"5000\"))\n```\n\n```sh:python-service/requirements.txt\nflask\nflask_cors\ndapr\ndapr-ext-grpc\ndapr-ext-fastapi\n```\n\n<br />\n\n依存関係をインストールします。\n\n```shellsession\n$ cd python-service\n$ pip3 install -r requirements.txt\n```\n\n<br />\n\n起動するかどうか確認します。\n\n```shellsession\n$ python3 app.py\n```\n\n<br />\n\n`* Running on http://127.0.0.1:5000` と表示されたら、正常ですので、CTRL + C で止めます。\n\n<br />\n\n<blockquote class=\"info\">\n<p>Dapr に関係するキモとなる実装は、</p>\n<p><code>from dapr.clients import DaprClient</code></p>\n<p>の部分と、</p>\n<p><code>d.get_state(store_name=\"orders\", key=id)</code></p>\n<p><code>d.save_state(store_name=\"orders\", key=id, value=json.dumps(request.json))</code></p>\n<p><code>d.delete_state(store_name=\"orders\", key=id)</code></p>\n<p>の部分です。</p>\n<p>状態管理コンポーネント(バックエンドDB)が何であってもこの実装のままでOKです。</p>\n</blockquote>\n\n<br />\n\n# Dapr インストール\n\n今回、完全にオンプレミスな環境で動作確認するため、Dapr CLI をインストールします。Kubernetes クラスター作成とかは行いません。\n\n```shellsession\n$ wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash\n$ dapr --version\nCLI version: 1.9.1\nRuntime version: n/a\n```\n\n<br />\n\nDapr を初期化します。  \nこれにより、Dapr サイドカーバイナリと ローカル開発環境の Docker のコンテナが作成されます。Docker のコンテナには、Redis container instance、Zipkin container instance、default components folder、Dapr placement service container instance、が作成されます。  \n今回関係するのは、Redis container instance と default components folder で、Redis state store が状態管理のバックエンド DB として使えます。  \n状態管理 DB の設定は、デフォルトでは、`~/.dapr/components/statestore.yaml` にあり、`statestore` という名前の `localhost:6379` の redis が使われるように設定されています。\n\n<blockquote class=\"info\">\n<p>Docker 無しでも初期化できますが、今回そちらは、説明しません。</p>\n</blockquote>\n\n```shellsession\n$ dapr init\n$ dapr --version\nCLI version: 1.9.1\nRuntime version: 1.9.5\n$ docker ps\nCONTAINER ID   IMAGE               COMMAND                  CREATED         STATUS                   PORTS                                                 NAMES\nee3efe641a43   redis:6             \"docker-entrypoint.s…\"   2 minutes ago   Up 2 minutes             0.0.0.0:6379->6379/tcp, :::6379->6379/tcp             dapr_redis\nffbeaf1d4bcc   daprio/dapr:1.9.5   \"./placement\"            2 minutes ago   Up 2 minutes             0.0.0.0:50005->50005/tcp, :::50005->50005/tcp         dapr_placement\ne2f9f31afec8   openzipkin/zipkin   \"start-zipkin\"           2 minutes ago   Up 2 minutes (healthy)   9410/tcp, 0.0.0.0:9411->9411/tcp, :::9411->9411/tcp   dapr_zipkin\n```\n\n<br />\n\n# Dapr Run\n\n基本的に、  \n`dapr run --app-id <アプリ名> --app-port <アプリのポート> --dapr-http-port <daprのポート> <起動コマンド>`  \nで起動するのですが、その前にやっておかないといけないことが2点あります。\n\n<br />\n\n**・画面(React)のビルド**  \n\n画面は、node.js の HTTP サーバーから `client/build/index.html` を参照するだけにしたいため、ビルドします。\n\n```shellsession\n$ cd node-service\n$ npm run buildclient\n```\n\n<br />\n\n**・statestore.yaml**\n\n今回、ストア名(DB名)を `statestore` ではなく、 `orders` で実装しているため、`statestore.yaml` を別に作成しておきます。  \n\n```shellsession\n$ mkdir -p dapr-components/local\n$ vi dapr-components/local/statestore.yaml\n```\n\n```yaml:dapr-components/local/statestore.yaml\napiVersion: dapr.io/v1alpha1\nkind: Component\nmetadata:\n  name: orders\nspec:\n  type: state.redis\n  version: v1\n  metadata:\n    - name: redisHost\n      value: localhost:6379\n    - name: redisPassword\n      value: \"\"\nscopes:\n  - python-app\n```\n\n<br />\n\nここまでで、以下の状況です。(`node-service/node_modules`、`node-service/client/node_modules`、`node-service/client/build` は除外しています。)\n\n```sh\n~/hello-dapr-app\n|-- dapr-components\n|   `-- local\n|       `-- statestore.yaml\n|-- node-service\n|   |-- client\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|   |-- index.js\n|   |-- package.json\n|   `-- package-lock.json\n`-- python-service\n    |-- app.py\n    `-- requirements.txt\n```\n\n<br />\n\n準備が終わったら、いよいよ起動です。  \n\n<br />\n\n**・python 起動**  \n\n`dapr-components/local/statestore.yaml` を指定して起動します。  \n\n```shellsession\n$ cd python-service\n$ dapr run --app-id python-app --app-port 5000 --dapr-http-port 3500 --components-path ../dapr-components/local python3 app.py\n```\n\n<br />\n\n**・node 起動**  \n\nnode の方は、状態管理が無いため、`--components-path` 無しで起動します。  \n\n```shellsession\n$ cd node-service\n$ dapr run --app-id node-app --app-port 3000 --dapr-http-port 3501 npm start\n```\n\n<br />\n\n# 動作確認\n\n**1.** データ無しの状態で、GET してみます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/image2.png\" alt=\"データ無しの状態で、GET\" width=\"652\" height=\"218\" loading=\"lazy\"></a>\n\nデータが無いという結果が返りました。\n\n<br />\n\n**2.** POST してデータを入れます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/image3.png\" alt=\"POST してデータ入力\" width=\"652\" height=\"204\" loading=\"lazy\"></a>\n\n<blockquote class=\"info\">\n<p>実装を最小限にするため、データは固定です。</p>\n</blockquote>\n\n<br />\n\n**3.** 再びデータを GET します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/image4.png\" alt=\"再びデータを GET\" width=\"652\" height=\"207\" loading=\"lazy\"></a>\n\nデータが取り出されました。\n\n<br />\n\n**4.** データを DELETE します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/image5.png\" alt=\"データを DELETE\" width=\"655\" height=\"144\" loading=\"lazy\"></a>\n削除に成功しました。\n\n<br />\n\n**5.** 再びデータを GET します。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/image2.png\" alt=\"削除後、再びデータを GET\" width=\"652\" height=\"218\" loading=\"lazy\"></a>\nデータ無しに戻ります。\n\n<br />\n\nちなみに、DB が存在しなかった場合、以下のエラーになります。今回の場合、`--components-path` 無しで python を起動するとこうなります。  \n<span style=\"color: #e70500;background-color: #ffebe7;\">`<_InactiveRpcError of RPC that terminated with:\\n\\tstatus = StatusCode.INVALID_ARGUMENT\\n\\tdetails = \\\"state store orders is not found...`</span>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/image6.png\" alt=\"DB が存在しなかった場合 エラー\" width=\"652\" height=\"288\" loading=\"lazy\"></a>\n\n<br />\n\n通信先の Dapr サイドカーが無いとき、以下のエラーになります。(あくまで今回の実装の場合です。)  \n<span style=\"color: #e70500;background-color: #ffebe7;\">`AxiosError: Request failed with status code 500`</span>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/image7.png\" alt=\"Dapr サイドカーが無いとき エラー\" width=\"655\" height=\"232\" loading=\"lazy\"></a>\n\n<br />\n\nヨシ!\n\n<br />\n\n続きの記事へ →「<a href=\"https://itc-engineering-blog.netlify.app/blogs/azure-aca-dapr-bicep\" target=\"_blank\">Bicepを使ってAzure Container AppsとDaprのマイクロサービスをデプロイ</a>」\n","description":"Daprのサービス呼び出しと状態管理を利用する簡易Webアプリを作成しました。何もない状態から実装を進めて、Dapr CLIを使って動作確認まで実施します。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"5b7zqqdmtqk","createdAt":"2022-12-06T13:52:24.653Z","updatedAt":"2022-12-06T13:52:24.653Z","publishedAt":"2022-12-06T13:52:24.653Z","revisedAt":"2022-12-06T13:52:24.653Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/dapr-local-dev/ITC_Engineering_Blog.png","alt":"Node.js,Python,ReactでDaprの状態管理アプリを作成してローカル環境で動作確認","width":1200,"height":630}],"seo_authors":[]},{"id":"nextjs-graphql-react-query","createdAt":"2022-12-30T12:14:15.818Z","updatedAt":"2023-05-10T07:08:28.932Z","publishedAt":"2022-12-30T12:14:15.818Z","revisedAt":"2023-05-10T07:08:28.932Z","title":"Next.js, graphql-codegen, React Query, Apollo Server v4 で簡易BFF作成(2/2)","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":"m0cpzdya8l3","createdAt":"2022-12-30T11:35:33.149Z","updatedAt":"2022-12-30T11:35:33.149Z","publishedAt":"2022-12-30T11:35:33.149Z","revisedAt":"2022-12-30T11:35:33.149Z","topics":"GraphQL","logo":"/logos/GraphQL.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\nNext.js v13、graphql-codegen 2.16.1、React v18、TypeScript、TanStack Query v4(React Query)、Apollo Server v4 で簡易 BFF を作成しました。  \n<a href=\"https://itc-engineering-blog.netlify.app/blogs/nextjs-graphql-bff-apollo-server\" target=\"_blank\">前回</a>は、サーバーサイド、バックエンド部分(Next.js の `pages/api/graphql`)を作成してきましたが、今回は、クライアントサイド、フロントエンド部分(`pages/_app.tsx`、`pages/index.tsx`)を作成していきます。\n\n<br />\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-react-query/douga1.gif\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-react-query/douga1.gif\" alt=\"クライアント動作内容 動画\" width=\"500\" height=\"352\" loading=\"lazy\"></a>\n\n<br />\n\n1.codegen 利用からサーバーサイドの実装、動作確認(<a href=\"https://itc-engineering-blog.netlify.app/blogs/nextjs-graphql-bff-apollo-server\" target=\"_blank\">前回の記事</a>)  \n2.クライアントサイドの実装、動作確認(今回の記事)  \nになります。\n\n<blockquote class=\"warn\">\n<p>【検証環境】</p>\n<p><code>Ubuntu 20.04.2 LTS</code></p>\n<p> <code>node v14.20.0</code></p>\n<p> <code>npm 6.14.17/code></p>\n<p> <code>@apollo/server 4.3.0</code></p>\n<p> <code>graphql 16.6.0</code></p>\n<p> <code>next 13.1.1</code></p>\n<p> <code>react 18.2.0</code></p>\n<p> <code>typescript 4.9.4</code></p>\n<p> <code>@graphql-codegen/cli 2.16.2</code></p>\n<p> <code>graphql-request 5.1.0</code></p>\n<p> <code>@tanstack/react-query 4.20.4</code></p>\n<p><code>Windows 10 Pro x64</code></p>\n<p> <code>Visual Studio Code 1.74.2</code></p>\n</blockquote>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu1.png\" alt=\"GraphQL、Next.js、graphql-codegen、TanStack Query(React Query)、graphql-request、Apollo Server、BFF それぞれの役割\" width=\"961\" height=\"471\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<blockquote class=\"warn\">\n<p>各要素の意味については、<a href=\"https://itc-engineering-blog.netlify.app/blogs/nextjs-graphql-bff-apollo-server\" target=\"_blank\">前回の記事</a>で説明していますので、省略します。</p>\n<p>また、Next.js、React について基本的な事の説明はありません。いきなりソースコードを書き始めます。</p>\n</blockquote>\n\n<blockquote class=\"alert\">\n<p>graphql-codegen で自動生成した React Hooks を使います。TanStack Query、graphql-request 単独の使い方についての説明はこの記事にはありません。</p>\n<p>キャッシュの生存時間等、オプションをいろいろ使えますが、オプションについてはほとんど触れていません。</p>\n</blockquote>\n\n<br />\n\n# 今回の要件\n\n名前を返す API(`http://localhost:4000/names`)と住所を返す API(`http://localhost:5000/addresses`)があるとします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu9.png\" alt=\"名前を返すAPIと住所を返すAPI\" width=\"1302\" height=\"311\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n・フロントエンドから一つのエンドポイントに対してリクエスト  \n・一度のリクエストで、名前を返す API と住所を返す API の結果を一度に受け取る  \n・名前を返す API だけデータの追加、更新ができる  \n・名前を返す API だけから全データ取り出すことができる  \n・名前を返す API に id(何番目か)を指定して、指定の名前を1つだけ取り出すことができる  \n・住所を返す API だけから全データ取り出すことができる  \n・住所を返す API に id(何番目か)を指定して、指定の住所を1つだけ取り出すことができる\n\n<br />\n\n<span style=\"color: red;\">両 API を用いて実現できることについて、実用的な意味は無いです。</span>今回の場合、BFF によって、Rest API 群(場合によっては、gRPC などを使ったマイクロサービス群など)を束ねるというのが趣旨です。\n\n<br />\n\nこれに対して、GraphQL クエリーを実行するクライアントを作ります。  \nクライアントは、以下のことができるものとします。  \n・全ての名前と住所を返す GraphQL クエリーを実行 → レスポンスの JSON を画面に表示  \n・最初の名前だけを返す GraphQL クエリーを実行 → レスポンスの JSON を画面に表示  \n・名前を追加する GraphQL クエリーを実行(追加する名前は、ランダムとする。)→ 成功したら、全ての名前と住所を返す GraphQL クエリーを実行して、レスポンスの JSON を画面に表示  \n・最初の名前を更新する GraphQL クエリーを実行(更新する名前は、ランダムとする。)→ 成功したら、全ての名前と住所を返す GraphQL クエリーを実行して、レスポンスの JSON を画面に表示  \n\n<br />\n\n<a href=\"https://itc-engineering-blog.netlify.app/blogs/nextjs-graphql-bff-apollo-server\" target=\"_blank\">前回</a>は、Apollo Server の機能を使ってテストしましたが、自分で実装したクライアントで GraphSQL クエリを実行するということです。\n\n<br />\n\n# React Hooks 自動生成\n\n<a href=\"https://itc-engineering-blog.netlify.app/blogs/nextjs-graphql-bff-apollo-server\" target=\"_blank\">前回の記事</a>と重複しますが、この前提が無いと意味不明になりますので、おさらいします。  \ngraphql-codegen にて、GraphQL クエリーに合わせた React Hooks を生成します。\n\n<blockquote class=\"warn\">\n<p>大半の説明は省略します。</p>\n<p>詳細は、<a href=\"https://itc-engineering-blog.netlify.app/blogs/nextjs-graphql-bff-apollo-server\" target=\"_blank\">前回の記事</a>を参照してください。</p>\n</blockquote>\n\n<br />\n\n## graphql-codegen init\n\ngraphql-codegen 初期化作業を行います。\n\n```shellsession\n$ npx graphql-codegen init\n```\n\nここで、以下のように回答します。  \n**What type of application are you building?** `Application built with React`  \n**Where is your schema?: (path or url)** `pages/api/graphql/schema.ts`  \n**Where are your operations and fragments?:** `graphql/**/*.graphql`  \n**Where to write the output:** `generated/resolvers.d.ts`  \n**Do you want to generate an introspection file?** `Yes`  \n**How to name the config file?** `codegen.yml`  \n**What script in package.json should run the codegen?** `codegen`\n\n<blockquote class=\"info\">\n<p><code>codegen.ts</code> or <code>codegen.yml</code> が生成されて package.json に <code>\"codegen\": \"graphql-codegen --config codegen.yml\"</code> が書き込まれます。</p>\n</blockquote>\n\n<br />\n\n## codegen.yml 設定\n\n`codegen.yml` が自動生成されていますので、これを編集します。\n\n<br />\n\n```shellsession\n$ vi codegen.yml\n```\n\n```yaml:codegen.yml\n# 既に生成物がある場合、上書き\noverwrite: true\n# スキーマファイルの場所\nschema: pages/api/graphql/schema.ts\ngenerates:\n  # resolvers(サーバー側)の TypeScript 型定義生成設定\n  ./generated/resolvers.d.ts:\n    plugins:\n      - typescript\n      - typescript-resolvers\n    config:\n      useIndexSignature: true\n      contextType: ../../pages/api/graphql/resolvers#Context\n  ./generated/graphql.ts:\n    documents: \"graphql/**/*.graphql\"\n    plugins:\n      - typescript\n      # @graphql-codegen/typescript-resolvers\n      # documents から TypeScript 型定義を生成するプラグイン\n      - typescript-operations\n      # documents から React Hooksコード生成するプラグイン\n      - typescript-react-query\n    config:\n      # React Hooks の fetcher に graphql-request を使用\n      # 指定しないと fetch が使われる。\n      fetcher: graphql-request\n      # Hooks を生成するか否か。true,falseにしても生成された。(?)何も変わらないので、コメントアウトしておく。\n      # isReactHook: true\n      # 生成物に useGetNameQuery.getKey = (variables: GetNameQueryVariables) => ['getName', variables];\n      # のように QueryKey を取り出すことができるメソッドが追加される。(['getName', {id: '0'}]のような値がキーとして返る。)\n      # react-query は QueryKey を使用してキャッシュを管理する。\n      # setQueryData や getQueryData などのメソッドには、このキーが必要。\n      # 今回直接react-queryを使っていないため、結局使わない。\n      exposeQueryKeys: true\n  # introspectionファイルを作成\n  ./graphql.schema.json:\n    plugins:\n      - \"introspection\"\n```\n\n<br />\n\n自動で package.json に追加された `@graphql-codegen/client-preset` は不要なので、削除します。(末尾のカンマに注意。)\n\n```shellsession\n$ vi package.json\n```\n\n```json:package.json\n  \"devDependencies\": {\n    \"@graphql-codegen/cli\": \"2.16.2\",\n    \"@graphql-codegen/introspection\": \"2.2.3\",\n    \"@graphql-codegen/client-preset\": \"1.2.4\"\n  }\n```\n\n↓\n\n```json:package.json\n  \"devDependencies\": {\n    \"@graphql-codegen/cli\": \"2.16.2\",\n    \"@graphql-codegen/introspection\": \"2.2.3\"\n  }\n```\n\n<br />\n\n## schema 作成\n\nschema として指定した schema.ts を作成します。  \n<span style=\"color:red;\">注意:</span>サーバーサイドに関係します。今回のクライアントサイドでは関係ありません。\n\n```shellsession\n$ mkdir pages/api/graphql\n$ vi pages/api/graphql/schema.ts\n```\n\n```ts:pages/api/graphql/schema.ts\nimport { gql } from \"graphql-tag\";\n\nexport const typeDefs = gql`\n  type Name {\n    name: String!\n  }\n\n  input NameInput {\n    name: String!\n  }\n\n  type Address {\n    address: String!\n  }\n\n  type Query {\n    names: [Name!]\n    name(id: ID!): Name\n    addresses: [Address!]\n    address(id: ID!): Address\n  }\n\n  type Mutation {\n    addName(input: NameInput!): Name!\n    updateName(id: ID!, input: NameInput!): Name!\n  }\n`;\n```\n\n<br />\n\n## documents 作成\n\nクライアント側の documents を作成します。\n\n<br />\n\nQuery と Mutation があるため、2ファイルに分けます。(1ファイルに全部書いても、もっと細分化しても構いません。)\n\n```shellsession\n$ mkdir graphql\n$ vi graphql/queries.graphql\n$ vi graphql/mutations.graphql\n```\n\n```graphql:graphql/queries.graphql\nquery getNames {\n  names {\n    name\n  }\n}\nquery getName($id: ID!) {\n  name(id: $id) {\n    name\n  }\n}\nquery getAddresses {\n  addresses {\n    address\n  }\n}\nquery getAddress($id: ID!) {\n  address(id: $id) {\n    address\n  }\n}\nquery getNamesAndAddresses {\n  names {\n    name\n  }\n  addresses {\n    address\n  }\n}\nquery getNameAndAddress($id: ID!) {\n  name(id: $id) {\n    name\n  }\n  address(id: $id) {\n    address\n  }\n}\n```\n\n```graphql:graphql/mutations.graphql\nmutation addName($input: NameInput!) {\n  addName(input: $input) {\n    name\n  }\n}\nmutation updateName($id: ID!, $input: NameInput!) {\n  updateName(id: $id, input: $input) {\n    name\n  }\n}\n```\n\n<br />\n\n## codegen 実行\n\ngenerated/resolvers.d.ts  \ngenerated/graphql.ts  \ngraphql.schema.json  \nを生成します。\n\n<br />\n\n<span style=\"color: #e70500;background-color: #ffebe7;\">Unable to find template plugin matching 'typescript-react-query'</span>  \nのようにエラーになりますので、plugins に指定したパッケージを `--save-dev` でインストールしてから、実行します。\n\n```shellsession\n$ npm install --save-dev @graphql-codegen/typescript-operations\n$ npm install --save-dev @graphql-codegen/typescript-react-query\n$ npm install --save-dev @graphql-codegen/typescript-resolvers\n$ mkdir generated\n$ npm install\n$ npm run codegen\n```\n\n<br />\n\n<span style=\"color: red;\"><strong>今回は、<code>generated/graphql.ts</code> を使用します。ここに React Hooks が自動生成されています。</strong></span>\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-react-query/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-react-query/image1.png\" alt=\"graphql.ts実装の一部\" width=\"1201\" height=\"555\" loading=\"lazy\"></a>\n\n<br />\n\n# npm install\n\n<a href=\"https://itc-engineering-blog.netlify.app/blogs/nextjs-graphql-bff-apollo-server\" target=\"_blank\">前回の記事</a>で実行した `npm install` をおさらいすると、以下です。\n\n```shellsession\n$ npm install --save-dev @graphql-codegen/cli\n$ npm install graphql @apollo/server @apollo/datasource-rest graphql-tag\n$ npm install --save-dev @graphql-codegen/typescript-operations\n$ npm install --save-dev @graphql-codegen/typescript-react-query\n$ npm install --save-dev @graphql-codegen/typescript-resolvers\n```\n\n<br />\n\n今回、クライアント側に必要な npm を追加します。\n\n```shellsession\n$ npm install graphql-request @tanstack/react-query @tanstack/react-query-devtools\n$ npm install react-toastify\n$ npm install unique-names-generator\n```\n\n<br />\n\n<span style=\"background-color: cornsilk;\"><strong>@tanstack/react-query / TanStack Query(React Query)</strong></span>  \nサーバー側のデータの取得と操作を容易にするために使用されるライブラリです。 React Query を使用すると、React アプリケーションでのサーバー状態の取得、キャッシュ、同期、および更新を簡単に行うことができます。  \nTanStack Query は、TS/JS、React、Solid、Vue で使えます。(Svelte も対応予定にあるようですが、2022 年 12 月現在まだ開発中のようです。)\n\n<br />\n\n<span style=\"background-color: cornsilk;\"><strong>@tanstack/react-query-devtools</strong></span>  \nReact Query 専用の開発ツールです。React Query のすべての内部動作を視覚化するのに役立ち、ピンチに陥った場合にデバッグの時間を節約できる可能性があります。\n\n<blockquote class=\"warn\">\n<p>作成するクライアントがあまりに殺風景のため、デフォルトでオンとします。使い方の説明はこの記事にはありません。</p>\n</blockquote>\n\n<br />\n\n<span style=\"background-color: cornsilk;\"><strong>graphql-request</strong></span>  \nシンプルかつ軽量な GraphQL クライアントです。プロミス(非同期処理の最終的な完了もしくは失敗を表すオブジェクト)に対応しています。TypeScript をサポートしています。Node のサーバーサイド/ブラウザ側で使用できます。今回は、ブラウザ側で使用しています。\n\n<br />\n\n<span style=\"background-color: cornsilk;\"><strong>react-toastify</strong></span>  \n以下のようにトースト(スッと表れてスッと消える通知メッセージ)を実現するために導入します。  \n特に React Query とか、GraphQL とかには関係しません。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-react-query/douga2.gif\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-react-query/douga2.gif\" alt=\"トーストの動作例動画\" width=\"600\" height=\"119\" loading=\"lazy\"></a>\n\n<br />\n\n<span style=\"background-color: cornsilk;\"><strong>unique-names-generator</strong></span>  \nランダムに適当な名前を返すツールです。追加したり更新したりする名前が毎回同じとかだとつまらないので、これを使ってスターウォーズのキャラクター名がランダムに生成されるようにしました。\n\n<br />\n\n# \\_app.tsx 実装\n\n```shellsession\n$ vi pages/_app.tsx\n```\n\n<blockquote class=\"info\">\n<p>説明は、ソースコード内のコメントを参照してください。</p>\n</blockquote>\n\n最初、以下のようになっていると思いますが、完全に書き換えます。\n\n```tsx:pages/_app.tsx\nimport '../styles/globals.css'\nimport type { AppProps } from 'next/app'\n\nexport default function App({ Component, pageProps }: AppProps) {\n  return <Component {...pageProps} />\n}\n```\n\n```tsx:pages/_app.tsx\nimport \"../styles/globals.css\";\nimport type { AppProps } from \"next/app\";\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { ReactQueryDevtools } from \"@tanstack/react-query-devtools\";\nimport { ToastContainer } from \"react-toastify\";\n// ToastContainerコンポーネント配置と同時にReactToastify.cssを読み込むreact-toastifyの作法\nimport \"react-toastify/dist/ReactToastify.css\";\n\nconst queryClient = new QueryClient();\n\nexport default function App({ Component, pageProps }: AppProps) {\n  return (\n    // 全体を通して、React Queryを有効にするため、(今回は、index.tsxだけだが)\n    // QueryClientProviderで要素を包みQueryClientをインスタンス化したqueryclientを設定。(必須)\n    <QueryClientProvider client={queryClient}>\n      {/* react-toastify の ToastContainerコンポーネントを配置 */}\n      {/* 注意:index.tsxに配置しても機能しない。親要素に配置が必要。 */}\n      <ToastContainer />\n      <Component {...pageProps} />\n      {/* react-query-devtoolsを初期表示から有効する。 */}\n      {/* falseの場合、最初ボタンだけ表示されて、クリックしたら開く。 */}\n      <ReactQueryDevtools initialIsOpen={true} />\n    </QueryClientProvider>\n  );\n}\n```\n\n<br />\n\n# index.tsx 実装\n\n```shellsession\n$ vi pages/index.tsx\n```\n\n最初、以下のようになっていると思いますが、完全に書き換えます。\n\n<blockquote class=\"warn\">\n<p><span style=\"color:red;\"><strong>自動生成された <code>generated/graphql.ts</code> を使います。useQuery をそのまま使う場合と異なります。</strong></span></p>\n</blockquote>\n\n```tsx:pages/index.tsx\nimport Head from 'next/head'\nimport Image from 'next/image'\nimport { Inter } from '@next/font/google'\nimport styles from '../styles/Home.module.css'\n\nconst inter = Inter({ subsets: ['latin'] })\n\nexport default function Home() {\n  return (\n    <>\n      <Head>\n        <title>Create Next App</title>\n・・・\n```\n\n```tsx:pages/_app.tsx\nimport Head from \"next/head\";\nimport { GraphQLClient } from \"graphql-request\";\n// graphql-codegen を実行した生成物 graphql.ts から各フック関数を読み込み。\nimport {\n  useGetNamesAndAddressesQuery,\n  useGetNameQuery,\n  GetNamesAndAddressesQuery,\n  useAddNameMutation,\n  useUpdateNameMutation,\n} from \"../generated/graphql\";\nimport { useState } from \"react\";\n// 成功、エラーを表示するために、\n// トースト(スッと表れてスッと消える通知メッセージ)を利用\nimport { toast } from \"react-toastify\";\n// スターウォーズのキャラクタ名をランダムに返す\nimport { uniqueNamesGenerator, Config, starWars } from \"unique-names-generator\";\n\n// (alias) new GraphQLClient(url: string, options?: PatchedRequestInit | undefined): GraphQLClient\n// 今回は、オプション無しでシンプルにURLのみ指定。環境変数は使わないため、URL固定。\nconst graphQLClient = new GraphQLClient(\n  process.env.NEXT_PUBLIC_GRAPHQL_URL || \"http://localhost:3000/api/graphql\"\n);\n\n// unique-names-generator のスターウォーズのキャラクタ名を使う設定\nconst config: Config = {\n  dictionaries: [starWars],\n};\n\nexport default function Home() {\n  // GetNamesAndAddressesQuery型(名前&住所が入る)\n  // data は、画面表示に使う。\n  const [data, setData] = useState<GetNamesAndAddressesQuery | undefined>(\n    undefined\n  );\n\n  // 全ての名前と住所を返す GraphQL クエリーを実行→レスポンスのJSONを画面に表示\n  const res1 = useGetNamesAndAddressesQuery(\n    graphQLClient,\n    {}, // GraphQLクエリの引数無し\n    {\n      // ユーザーがブラウザのコンポーネントにフォーカスを当てた時に自動でフェッチが動くのを抑止\n      refetchOnWindowFocus: false,\n      // ボタンをクリックしたタイミングで起動するため、ここでは実行されない。\n      enabled: false,\n      // 成功したときのコールバック\n      onSuccess(data) {\n        // 取得したデータ(JSON)を画面に反映\n        setData(data);\n      },\n      onError(error: any) {\n        // エラーの時、トーストに表示(注意:エラーは複数返る可能性がある)\n        error.response.errors.forEach((err: any) => {\n          toast(err.message, {\n            type: \"error\", // error タイプのデザイン(赤色)\n            position: \"top-center\", // 真ん中の上から出現\n          });\n        });\n      },\n    }\n  );\n  // 最初の名前だけを返す GraphQL クエリーを実行→レスポンスのJSONを画面に表示\n  const res2 = useGetNameQuery(\n    graphQLClient,\n    { id: \"0\" }, // GraphQLクエリの引数に最初の名前と指定(ハードコード)\n    {\n      refetchOnWindowFocus: false,\n      enabled: false,\n      onSuccess(data) {\n        setData(data);\n      },\n      onError(error: any) {\n        error.response.errors.forEach((err: any) => {\n          toast(err.message, {\n            type: \"error\",\n            position: \"top-center\",\n          });\n        });\n      },\n    }\n  );\n  // 名前を追加する GraphQL クエリーを実行(追加する名前は、ランダムとする。)\n  // →成功したら、全ての名前と住所を返す GraphQL クエリーを実行して、レスポンスのJSONを画面に表示\n  // Mutation系(取得ではなく、更新系)は、ここでは実行されない。\n  const res3 = useAddNameMutation(graphQLClient, {\n    onSuccess() {\n      // \"Name created successfully\" とトースト表示\n      toast(\"Name created successfully\", {\n        autoClose: 1000, // 1秒で消えるように設定\n        type: \"success\", // success タイプのデザイン(緑色)\n        position: \"top-center\", // 真ん中の上から出現\n      });\n      res1.refetch(); // 全ての名前と住所を返す GraphQL クエリーを実行\n    },\n    onError(error: any) {\n      error.response.errors.forEach((err: any) => {\n        toast(err.message, {\n          type: \"error\",\n          position: \"top-center\",\n        });\n      });\n    },\n  });\n  // 最初の名前を更新する GraphQL クエリーを実行(更新する名前は、ランダムとする。)\n  // →成功したら、全ての名前と住所を返す GraphQL クエリーを実行して、レスポンスのJSONを画面に表示\n  const res4 = useUpdateNameMutation(graphQLClient, {\n    onSuccess() {\n      toast(\"Name updated successfully\", {\n        autoClose: 1000,\n        type: \"success\",\n        position: \"top-center\",\n      });\n      res1.refetch(); // 全ての名前と住所を返す GraphQL クエリーを実行\n    },\n    onError(error: any) {\n      error.response.errors.forEach((err: any) => {\n        toast(err.message, {\n          type: \"error\",\n          position: \"top-center\",\n        });\n      });\n    },\n  });\n\n  // GetNamesAndAddressesQueryボタンクリック\n  const onGetNamesAndAddressesQuery = async () => {\n    res1.refetch(); // 全ての名前と住所を返す GraphQL クエリーを実行\n  };\n\n  // GetNameQueryボタンクリック\n  const onGetNameQuery = async () => {\n    res2.refetch(); // 最初の名前だけを返す GraphQL クエリーを実行\n    // 注意:引数 { id: \"0\" } は、res2 作成時に指定している。\n  };\n\n  // AddNameMutationボタンクリック\n  const onAddNameMutation = async () => {\n    // スターウォーズのキャラクタ名をランダムに生成\n    const characterName: string = uniqueNamesGenerator(config);\n    res3.mutate({ input: { name: characterName } }); // 名前を追加する GraphQL クエリーを実行\n    // 注意:引数 { input: { name: characterName } } は、ここで指定。\n  };\n\n  // UpdateNameMutationボタンクリック\n  const onUpdateNameMutation = async () => {\n    const characterName: string = uniqueNamesGenerator(config); // 最初の名前を更新する GraphQL クエリーを実行\n    res4.mutate({ id: \"0\", input: { name: characterName } });\n    // 注意:引数 { id: \"0\", input: { name: characterName } } は、ここで指定。\n  };\n\n  return (\n    <>\n      <Head>\n        <title>Next App</title>\n        <meta name=\"description\" content=\"Generated by create next app\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <link rel=\"icon\" href=\"/favicon.ico\" />\n      </Head>\n      <button onClick={onGetNamesAndAddressesQuery}>\n        GetNamesAndAddressesQuery\n      </button>{\" \"}\n      <button onClick={onGetNameQuery}>GetNameQuery</button>{\" \"}\n      <button onClick={onAddNameMutation}>AddNameMutation</button>{\" \"}\n      <button onClick={onUpdateNameMutation}>UpdateNameMutation</button>\n      {/* GraphQLクエリ実行の結果JSONをスペース2個の文字列に整形して<pre></pre>タグで表示 */}\n      <pre>{JSON.stringify(data, null, 2)}</pre>\n    </>\n  );\n}\n```\n\n<br />\n\n完成しました!\n\n<br />\n\n# 動作確認\n\n```shellsession\n$ npm run dev\n```\n\n<br />\n\n`http://localhost:3000` にアクセスします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-react-query/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-react-query/image2.png\" alt=\"アクセス直後\" width=\"958\" height=\"753\" loading=\"lazy\"></a>\n\n<br />\n\nGetNamesAndAddressesQuery ボタンをクリックして、全ての名前と住所を得ます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-react-query/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-react-query/image3.png\" alt=\"GetNamesAndAddressesQuery ボタンをクリック\" width=\"616\" height=\"347\" loading=\"lazy\"></a>\n\nヨシ!\n\n<br />\n\nGetNameQuery ボタンをクリックして、最初の名前だけを得ます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-react-query/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-react-query/image4.png\" alt=\"GetNameQuery ボタンをクリック\" width=\"575\" height=\"207\" loading=\"lazy\"></a>\n\nヨシ!\n\n<br />\n\nAddNameMutation ボタンをクリックして、名前を追加します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-react-query/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-react-query/image5.png\" alt=\"AddNameMutation ボタンをクリック\" width=\"589\" height=\"379\" loading=\"lazy\"></a>\n\nヨシ!\n\n<br />\n\nUpdateNameMutation ボタンをクリックして、最初の名前を更新します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-react-query/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-react-query/image6.png\" alt=\"UpdateNameMutation ボタンをクリック\" width=\"592\" height=\"386\" loading=\"lazy\"></a>\n\nヨシッ!!\n\n<br />\n\nちなみにですが、エラーになると、以下の挙動になります。  \n(データソースの REST API を落として、接続エラーの例)  \n注意:実際は、リトライするため、エラー表示までに時間がかかりました。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-react-query/douga3.gif\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-react-query/douga3.gif\" alt=\"データソースの REST API を落として、接続エラーの例 動画\" width=\"600\" height=\"288\" loading=\"lazy\"></a>\n\n<br />\n","description":"【クライアント側作業編2/2】Next.js v13、graphql-codegen 2.16.1、React v18、TypeScript、TanStack Query v4(React Query)、Apollo Server v4 で簡易 BFF を作成しました。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"6ubb_la-wj","createdAt":"2022-12-30T12:02:23.814Z","updatedAt":"2022-12-30T12:02:23.814Z","publishedAt":"2022-12-30T12:02:23.814Z","revisedAt":"2022-12-30T12:02:23.814Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-react-query/ITC_Engineering_Blog.png","alt":"Next.js, graphql-codegen, React Query, Apollo Server v4 で簡易BFF作成(2/2)","width":1200,"height":630}],"seo_authors":[]},{"id":"nextjs-graphql-bff-apollo-server","createdAt":"2022-12-30T12:12:29.515Z","updatedAt":"2023-05-10T07:08:02.984Z","publishedAt":"2022-12-30T12:12:29.515Z","revisedAt":"2023-05-10T07:08:02.984Z","title":"Next.js, graphql-codegen, React Query, Apollo Server v4 で簡易BFF作成(1/2)","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":"m0cpzdya8l3","createdAt":"2022-12-30T11:35:33.149Z","updatedAt":"2022-12-30T11:35:33.149Z","publishedAt":"2022-12-30T11:35:33.149Z","revisedAt":"2022-12-30T11:35:33.149Z","topics":"GraphQL","logo":"/logos/GraphQL.png","needs_title":false},{"id":"0_vp_og7mo","createdAt":"2022-12-30T11:35:51.182Z","updatedAt":"2022-12-30T11:35:51.182Z","publishedAt":"2022-12-30T11:35:51.182Z","revisedAt":"2022-12-30T11:35:51.182Z","topics":"Apollo","logo":"/logos/Apollo.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}],"content":"# はじめに\n\nNext.js v13、graphql-codegen 2.16.1、React v18、TypeScript、TanStack Query v4(React Query)、Apollo Server v4 で簡易 BFF を作成しました。  \nその全手順とソースコードを何も無い状態から書いていこうと思います。  \nベストプラクティス的なつもりはなく、とりあえず動くところまでがゴールです。  \n2022 年 12 月現在、あまりに情報が少なく(特に Apollo Server v4 の場合)、とりあえずのとっかかりになれば幸いに思います。\n\n<br />\n\n長くなるため、2編に分けました。  \n1.codegen 利用からサーバーサイドの実装、動作確認(今回の記事)  \n2.クライアントサイドの実装、動作確認(<a href=\"https://itc-engineering-blog.netlify.app/blogs/nextjs-graphql-react-query\" target=\"_blank\">次回の記事</a>)  \nになります。\n\n<blockquote class=\"warn\">\n<p>【検証環境】</p>\n<p><code>Ubuntu 20.04.2 LTS</code></p>\n<p> <code>node v14.20.0</code></p>\n<p> <code>npm 6.14.17/code></p>\n<p> <code>@apollo/server 4.3.0</code></p>\n<p> <code>graphql 16.6.0</code></p>\n<p> <code>next 13.1.1</code></p>\n<p> <code>react 18.2.0</code></p>\n<p> <code>typescript 4.9.4</code></p>\n<p> <code>@graphql-codegen/cli 2.16.2</code></p>\n<p><code>Windows 10 Pro x64</code></p>\n<p> <code>Visual Studio Code 1.74.2</code></p>\n</blockquote>\n\n<blockquote class=\"warn\">\n<p>前置きが長いです。作業開始は、こちらからになります。</p>\n<p>↓</p>\n<p><a href=\"#start\">Next.js プロジェクト作成</a></p>\n</blockquote>\n\n<br />\n\n# 利用技術・用語\n\nまず、GraphQL、Next.js、graphql-codegen、TanStack Query(React Query)、graphql-request、Apollo Server、BFF について、簡単に説明します。\n\n<br />\n\nそれぞれの役割を図示すると以下になります。  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu1.png\" alt=\"GraphQL、Next.js、graphql-codegen、TanStack Query(React Query)、graphql-request、Apollo Server、BFF それぞれの役割\" width=\"961\" height=\"471\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<span style=\"color: red;\">注意:</span>図は、あくまで今回の場合です。\n\n<br />\n\n<span style=\"background-color: cornsilk;\"><strong>GraphQL</strong></span>  \nGraphQL は API 向けに作られたクエリ言語およびランタイムです。GraphQL では、クライアントが必要なデータの構造を定義することができ、サーバーからは定義したのと同じ構造のデータが返されます。  \nREST API と異なり、エンドポイント(呼び出し先 URL)が一つです。\n\n<br />\n\n・REST API の場合  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu2.png\" alt=\"REST API の場合\" width=\"801\" height=\"346\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n・GraphQL の場合  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu3.png\" alt=\"GraphQL の場合\" width=\"1222\" height=\"346\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n<span style=\"background-color: cornsilk;\"><strong>Next.js</strong></span>  \nNext.js を選定した理由は、以下の図のようにフロントエンドとサーバーサイドの API が簡単に同時に実装できるためです。`pages/api/xxx` に実装すれば、それが API として振る舞います。ルーティングの設定は不要です。  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu4.png\" alt=\"Next.jsを選定した理由\" width=\"602\" height=\"408\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\nもちろん、以下のように別の方法はいろいろ考えられます。\n\n<br />\n\n・Node サーバーを使う  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu5.png\" alt=\"Nodeサーバーを使う\" width=\"839\" height=\"381\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n・リバプロを使う  \n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu6.png\" alt=\"リバプロを使う\" width=\"760\" height=\"381\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n<span style=\"background-color: cornsilk;\"><strong>React、TypeScript</strong></span>  \nReact、TypeScript については、言わずもがなと思うので、簡単に言及します。  \nReact は、Next.js を使うからには、使う以外選択肢がありません。Next.js が React ベースのフレームワークだからです。  \nTypeScript は、型安全に実装したいため、選択しました。\n\n<br />\n\n<span style=\"background-color: cornsilk;\"><strong>graphql-codegen</strong></span>  \ngraphql-codegen は、GraphQL のスキーマから TypeScript の型が自動生成されます。さらに、ドキュメントから React Hooks が自動生成されます。(React Hooks は<a href=\"https://itc-engineering-blog.netlify.app/blogs/nextjs-graphql-react-query\" target=\"_blank\">次回</a>に活用します。)\n\n<blockquote class=\"warn\">\n<p>上記の説明は、正しくは、そういう設定したらという場合で、いろいろな自動生成パターンが考えられます。</p>\n</blockquote>\n\n<br />\n\n<span style=\"background-color: cornsilk;\"><strong>TanStack Query(React Query)</strong></span>  \nサーバー側のデータの取得と操作を容易にするために使用されるライブラリです。 React Query を使用すると、React アプリケーションでのサーバー状態の取得、キャッシュ、同期、および更新を簡単に行うことができます。  \nTanStack Query は、TS/JS、React、Solid、Vue で使えます。(Svelte も対応予定にあるようですが、2022 年 12 月現在まだ開発中のようです。)\n\n<br />\n\n<span style=\"background-color: cornsilk;\"><strong>graphql-request</strong></span>  \nシンプルかつ軽量な GraphQL クライアントです。プロミス(非同期処理の最終的な完了もしくは失敗を表すオブジェクト)に対応しています。TypeScript をサポートしています。Node のサーバーサイド/ブラウザ側で使用できます。今回は、ブラウザ側で使用しています。\n\n<br />\n\n<span style=\"background-color: cornsilk;\"><strong>Apollo Server</strong></span>  \nApollo Server は、オープンソースで仕様に準拠した GraphQL サーバーであり、Apollo Client を含むすべての GraphQL クライアントと互換性があります。  \nGET でアクセスすると、実装したサーバーに対して、GraphQL のクエリを直接実行できる画面が表示されます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/image1.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/image1.png\" alt=\"GraphQLのクエリを直接実行できる画面\" width=\"1200\" height=\"613\" loading=\"lazy\"></a>\n\n<span style=\"color: red;\"><strong>なお、今回、v4(<code>apollo-server</code>ではなく、<code>@apollo/server</code>)を使います。</strong></span>\n\n<br />\n\n<span style=\"background-color: cornsilk;\"><strong>BFF</strong></span>  \nBFF は、Backend For Frontend の略です。特定の言語やアプリケーションを指すのではなく、概念的なことです。  \nクライアントとバックエンドの間に位置して、クライアントが望むようにデータを加工して返します。  \n例えば、今回の場合、2つの REST API からデータを取得しますが、フロントエンドは、この BFF に1回だけ要求すると2つの REST API の結果を受け取ることができます。\n\n<br />\n\n・BFF が無いとき\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu7.png\" alt=\"BFFが無いとき\" width=\"893\" height=\"261\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n・BFF があるとき\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu8.png\" alt=\"BFFがあるとき\" width=\"1393\" height=\"261\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<br />\n\n# 今回の要件\n\n名前を返す API(`http://localhost:4000/names`)と住所を返す API(`http://localhost:5000/addresses`)があるとします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu9.png\" alt=\"名前を返すAPIと住所を返すAPI\" width=\"1302\" height=\"311\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n・フロントエンドから一つのエンドポイントに対してリクエスト  \n・一度のリクエストで、名前を返す API と住所を返す API の結果を一度に受け取る  \n・名前を返す API だけデータの追加、更新ができる  \n・名前を返す API だけから全データ取り出すことができる  \n・名前を返す API に id(何番目か)を指定して、指定の名前を1つだけ取り出すことができる  \n・住所を返す API だけから全データ取り出すことができる  \n・住所を返す API に id(何番目か)を指定して、指定の住所を1つだけ取り出すことができる\n\n<br />\n\n<span style=\"color: red;\">両 API を用いて実現できることについて、実用的な意味は無いです。</span>今回の場合、BFF によって、Rest API 群(場合によっては、gRPC などを使ったマイクロサービス群など)を束ねるというのが趣旨です。\n\n<br />\n\n今回では、BFF と称するのは大げさですが、以下のようにキャッシュの仕組みを組み込んだり、データソース(データの出どころ)が REST だったり、gRPC だったり、DB だったり、文字通りフロントエンドのためにいろいろできると思います。  \n今回は、一つでも余計な事をしようとすると、それだけで一記事になりますので、何もテクニカルな工夫をしていませんし、何も意味を持たせていません。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu10.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/zu10.png\" alt=\"BFFを活用した場合\" width=\"1392\" height=\"446\" style=\"margin-top: 5px;margin-bottom: 5px;\" loading=\"lazy\"></a>\n\n<blockquote class=\"warn\">\n<p>BFF の規模が大きくなると、Next.js ではかえって対応しにくくなるかもしれません。</p>\n</blockquote>\n\n<br />\n\n<a class=\"anchor\" id=\"start\"></a>\n\n# Next.js プロジェクト作成\n\ncreate-next-app で TypeScript 対応 Next.js 雛形プロジェクトを作成します。  \n`Would you like to use ESLint with this project?` は `Yes` を選択します。  \nプロジェクト名は、 next-bff-app とします。\n\n```shellsession\n$ npx create-next-app@latest next-bff-app --typescript\nnpx: installed 1 in 1.854s\n? Would you like to use ESLint with this project? … No / Yes\n```\n\nこの時点では、以下のようになっています。(`node_modules/` は除外)\n\n```shellsession\nnext-bff-app\n├── next.config.js\n├── next-env.d.ts\n├── package.json\n├── package-lock.json\n├── pages\n│   ├── api\n│   │   └── hello.ts\n│   ├── _app.tsx\n│   ├── _document.tsx\n│   └── index.tsx\n├── public\n│   ├── favicon.ico\n│   ├── next.svg\n│   ├── thirteen.svg\n│   └── vercel.svg\n├── README.md\n├── styles\n│   ├── globals.css\n│   └── Home.module.css\n└── tsconfig.json\n```\n\n<br />\n\n`pages/api/hello.ts` が存在します。  \nこれは、`http://localhost:3000/api/hello` で起動する API です。\n\n```shellsession\n$ cd next-bff-app\n$ npm run dev\n```\n\n```shellsession\n$ curl http://localhost:3000/api/hello\n{\"name\":\"John Doe\"}\n```\n\n<br />\n\n# npm インストール\n\ngraphql-codegen 他、必要なものをインストールします。  \nただし、今回サーバー側の実装だけのため、サーバー側に必要なものだけインストールしています。  \nフロントエンドで必要なものは、<a href=\"https://itc-engineering-blog.netlify.app/blogs/nextjs-graphql-react-query\" target=\"_blank\">次回記事</a>で、追加インストールします。\n\n```shellsession\n$ npm install --save-dev @graphql-codegen/cli\n$ npm install graphql @apollo/server @apollo/datasource-rest graphql-tag\n```\n\n<blockquote class=\"warn\">\n<p><span style=\"color:red;\"><strong><code>@apollo/server</code> により Apollo Server v4 が入ります。</strong></span></p>\n<p><span style=\"color:red;\"><strong><code>apollo-server</code>とか、<code>apollo-server-micro</code> にすると、v4 未満(Deprecated)になります。</strong></span></p>\n</blockquote>\n\n<br />\n\n# graphql-codegen init\n\ngraphql-codegen 初期化作業を行います。\n\n```shellsession\n$ npx graphql-codegen init\n```\n\nここで、以下のように回答します。  \n**What type of application are you building?** `Application built with React`  \n**Where is your schema?: (path or url)** `pages/api/graphql/schema.ts`  \n**Where are your operations and fragments?:** `graphql/**/*.graphql`  \n**Where to write the output:** `generated/resolvers.d.ts`  \n**Do you want to generate an introspection file?** `Yes`  \n**How to name the config file?** `codegen.yml`  \n**What script in package.json should run the codegen?** `codegen`\n\n<blockquote class=\"info\">\n<p><code>codegen.ts</code> or <code>codegen.yml</code> が生成されて package.json に <code>\"codegen\": \"graphql-codegen --config codegen.yml\"</code> が書き込まれます。</p>\n</blockquote>\n\n<br />\n\n各質問内容の意味は、以下です。\n\n**`What type of application are you building?`**  \nどのフレームワーク/ライブラリを使ったアプリかを回答します。今回は、React のため、Application built with React です。\n\n<br />\n\n**`Where is your schema?: (path or url)`**  \nスキーマの場所を回答します。  \nスキーマとは、以下のような GraphQL 独自の型定義(RDB でいうテーブル定義のようなもの)です。\n\n```graphql\ntype Human implements Character {\n  id: ID!\n  name: String!\n  friends: [Character]\n  appearsIn: [Episode]!\n  starships: [Starship]\n  totalCredits: Int\n}\n\ntype Droid implements Character {\n  id: ID!\n  name: String!\n  friends: [Character]\n  appearsIn: [Episode]!\n  primaryFunction: String\n}\n```\n\n<br />\n\n対象となる GraphQL サーバーの URL から直接取得できますが、今回、その GraphQL サーバーを作るところから始めますので、GraphQL サーバーのソースコード内から取り込まれるようにします。\n\n```ts:pages/api/graphql/schemas.ts\nimport { gql } from \"graphql-tag\";\n\nexport const typeDefs = gql`\n  type Name {\n    name: String!\n  }\n・・・略・・・\n  type Mutation {\n    addName(input: NameInput!): Name!\n    updateName(id: ID!, input: NameInput!): Name!\n  }\n`;\n\n```\n\n<br />\n\n**`Where are your operations and fragments?`**  \n`operations and fragments` がどこにあるか回答します。  \n`operations and fragments` とは、`documents: ` のことで、ドキュメントとは、すなわち、クライアントから GraphQL サーバーに対して、どのようなクエリーを使うつもりかが書かれたものです。  \n例えば、この質問に `src/**/*.tsx` と回答する場合、  \n`codegen.yml` に  \n`documents: src/**/*.tsx` が設定されて、\n\n```tsx:src/App.tsx\nconst allFilmsWithVariablesQueryDocument = graphql(/* GraphQL */ `\n  query allFilmsWithVariablesQuery($first: Int!) {\n    allFilms(first: $first) {\n      edges {\n        node {\n          ...FilmItem\n        }\n      }\n    }\n  }\n`);\n```\n\nのような実装がある場合、\n\n```graphql\nquery allFilmsWithVariablesQuery($first: Int!) {\n  allFilms(first: $first) {\n    edges {\n      node {\n        ...FilmItem\n      }\n    }\n  }\n}\n```\n\nの部分がドキュメントとして、取り込まれます。\n\n<blockquote class=\"info\">\n<p><code>src/**/*.tsx</code> の ** は、0~複数階層ディレクトリがあるという意味で、src/xxx.tsx、src/aaa/xxx.tsx、src/aaa/bbb/xxx.tsx などがマッチします。</p>\n</blockquote>\n\nドキュメントは、直接書いても良いため、今回の場合、直接書きます。\n\n<br />\n\n**`Where to write the output`**  \n生成物をどこに出力するかを設定します。  \nとりあえず、resolvers の型定義ファイル生成先 `generated/resolvers.d.ts` を設定します。  \n(詳細は、後の設定の説明を見てください。)\n\n<br />\n\n**`Do you want to generate an introspection file?`**  \nintrospection 用のファイルを生成するかどうかを回答します。  \nintrospection とは、特殊なクエリで、GraphQL サーバー自身が持っている内部情報(データの型、使えるクエリなどの情報)を返す機能です。  \n必須ではないですが、生成することにします。\n\n<br />\n\n・introspection クエリとレスポンスの例\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/image2.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/image2.png\" alt=\"introspectionクエリとレスポンスの例\" width=\"1174\" height=\"718\" loading=\"lazy\"></a>\n\n<blockquote class=\"warn\">\n<p>introspection file生成無しにしても Apollo Serverで問い合わせて、introspection が取得できました。出力された json について、どう活用するのか良く分かりませんでした。</p>\n</blockquote>\n\n<br />\n\n**`How to name the config file?`**  \ngraphql-codegen の設定ファイル名を回答します。  \n`codegen.ts` と設定すると、TypeScript 形式で設定を書くことになりますが、yaml で書きたいため、codegen.yml としました。\n\n<br />\n\n**`What script in package.json should run the codegen?`**  \ngraphql-codegen を起動するときのコマンド、`npm run codegen` の \"codegen\" の部分を回答します。`npm run codegen` で良いため、codegen と回答しました。\n\n<br />\n\n# codegen.yml 設定\n\n`codegen.yml` が自動生成されていますので、これを編集します。\n\n```yaml:codegen直後のcodegen.yml\noverwrite: true\nschema: \"pages/api/graphql/schema.ts\"\ndocuments: \"graphql/**/*.graphql\"\ngenerates:\n  generated/resolvers.d.ts:\n    preset: \"client\"\n    plugins: []\n  ./graphql.schema.json:\n    plugins:\n      - \"introspection\"\n```\n\n<br />\n\n自動生成された  \n`preset: \"client\"`  \nについては、プリセット設定で、これにより React、Svelte、Vue、その他クライアント用のコードが生成されるようですが、今のところ情報が少なく、特に問題無かったため、採用を見送りました。  \nしたがって、ここでは、削除して、書き換えます。\n\n```shellsession\n$ vi codegen.yml\n```\n\n```yaml:codegen.yml\n# 既に生成物がある場合、上書き\noverwrite: true\n# スキーマファイルの場所\nschema: pages/api/graphql/schema.ts\ngenerates:\n  # resolvers(サーバー側)の TypeScript 型定義生成設定\n  ./generated/resolvers.d.ts:\n    plugins:\n      - typescript\n      # @graphql-codegen/typescript-resolvers\n      # resolvers の TypeScript 型定義を生成するプラグイン\n      - typescript-resolvers\n    config:\n      # Apollo Serverを使う場合\n      # useIndexSignature: true\n      # にする\n      # ...と書いてあるが、正確な意味は不明。\n      # https://the-guild.dev/graphql/codegen/plugins/typescript/typescript-resolvers\n      useIndexSignature: true\n      # context(Apollo Serverの共通処理や値。すなわち、今回の場合、dataSources)に型が付く。\n      # 生成される resolvers.d.ts に\n      # import { Context } from '../../../pages/api/graphql/resolvers';\n      # が追加されて、\n      # export type AddressResolvers<ContextType = any,・・・\n      # が\n      # export type AddressResolvers<ContextType = Context,・・・\n      # になる。\n      # 生成される resolvers.d.ts から相対パスで、指定。\n      # resolvers#Contextは、resolvers.tsのtype Contextを見てねという意味。\n      contextType: ../../pages/api/graphql/resolvers#Context\n  # generated/graphql.ts:は、クライアント側に必要。\n  # クライアント側のTypeScript型定義とReact Hooksコード生成設定\n  ./generated/graphql.ts:\n    documents: \"graphql/**/*.graphql\"\n    plugins:\n      - typescript\n      # @graphql-codegen/typescript-resolvers\n      # documents から TypeScript 型定義を生成するプラグイン\n      - typescript-operations\n      # documents から React Hooksコード生成するプラグイン\n      - typescript-react-query\n    config:\n      # React Hooks の fetcher に graphql-request を使用\n      # 指定しないと fetch が使われる。\n      fetcher: graphql-request\n      # Hooks を生成するか否か。true,falseにしても生成された。(?)何も変わらないので、コメントアウトしておく。\n      # isReactHook: true\n      # 生成物に useGetNameQuery.getKey = (variables: GetNameQueryVariables) => ['getName', variables];\n      # のように QueryKey を取り出すことができるメソッドが追加される。(['getName', {id: '0'}]のような値がキーとして返る。)\n      # react-query は QueryKey を使用してキャッシュを管理する。\n      # setQueryData や getQueryData などのメソッドには、このキーが必要。\n      # 今回直接react-queryを使っていないため、結局使わない。\n      exposeQueryKeys: true\n  # introspectionファイルを作成\n  ./graphql.schema.json:\n    plugins:\n      - \"introspection\"\n```\n\n<br />\n\n自動で package.json に追加された `@graphql-codegen/client-preset` は不要なので、削除します。(末尾のカンマに注意。)\n\n```shellsession\n$ vi package.json\n```\n\n```json:package.json\n  \"devDependencies\": {\n    \"@graphql-codegen/cli\": \"2.16.2\",\n    \"@graphql-codegen/introspection\": \"2.2.3\",\n    \"@graphql-codegen/client-preset\": \"1.2.4\"\n  }\n```\n\n↓\n\n```json:package.json\n  \"devDependencies\": {\n    \"@graphql-codegen/cli\": \"2.16.2\",\n    \"@graphql-codegen/introspection\": \"2.2.3\"\n  }\n```\n\n<br />\n\n# schema 作成\n\nschema として指定した schema.ts を作成します。\n\n```shellsession\n$ mkdir pages/api/graphql\n$ vi pages/api/graphql/schema.ts\n```\n\n```ts:pages/api/graphql/schema.ts\nimport { gql } from \"graphql-tag\";\n\nexport const typeDefs = gql`\n  type Name {\n    name: String!\n  }\n\n  input NameInput {\n    name: String!\n  }\n\n  type Address {\n    address: String!\n  }\n\n  type Query {\n    names: [Name!]\n    name(id: ID!): Name\n    addresses: [Address!]\n    address(id: ID!): Address\n  }\n\n  type Mutation {\n    addName(input: NameInput!): Name!\n    updateName(id: ID!, input: NameInput!): Name!\n  }\n`;\n```\n\n<blockquote class=\"warn\">\n<p><span style=\"color: red;\"><strong><code>gql</code> は、Apollo Server v4 から <code>import { gql } from 'apollo-server';</code> ではなくなり、graphql-tag からインポートが必要です。</strong></span></p>\n</blockquote>\n\n<br />\n\n# documents 作成\n\nクライアント側の documents を作成します。  \n今回は、サーバー側のみ実装を行いますので、必要ありませんが、<a href=\"https://itc-engineering-blog.netlify.app/blogs/nextjs-graphql-react-query\" target=\"_blank\">次回</a>のクライアント作成編に使うため、作成します。\n\n<br />\n\nQuery と Mutation があるため、2ファイルに分けます。(1ファイルに全部書いても、もっと細分化しても構いません。)\n\n```shellsession\n$ mkdir graphql\n$ vi graphql/queries.graphql\n$ vi graphql/mutations.graphql\n```\n\n```graphql:graphql/queries.graphql\nquery getNames {\n  names {\n    name\n  }\n}\nquery getName($id: ID!) {\n  name(id: $id) {\n    name\n  }\n}\nquery getAddresses {\n  addresses {\n    address\n  }\n}\nquery getAddress($id: ID!) {\n  address(id: $id) {\n    address\n  }\n}\nquery getNamesAndAddresses {\n  names {\n    name\n  }\n  addresses {\n    address\n  }\n}\nquery getNameAndAddress($id: ID!) {\n  name(id: $id) {\n    name\n  }\n  address(id: $id) {\n    address\n  }\n}\n```\n\n```graphql:graphql/mutations.graphql\nmutation addName($input: NameInput!) {\n  addName(input: $input) {\n    name\n  }\n}\nmutation updateName($id: ID!, $input: NameInput!) {\n  updateName(id: $id, input: $input) {\n    name\n  }\n}\n```\n\n<br />\n\n# codegen 実行\n\ngenerated/resolvers.d.ts  \ngenerated/graphql.ts  \ngraphql.schema.json  \nを生成します。\n\n<br />\n\n生成コマンドは、`graphql-codegen --config codegen.yml` ですが、package.json に自動的に追加されています。\n\n```json:package.json\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\",\n    \"codegen\": \"graphql-codegen --config codegen.yml\"\n  },\n```\n\nしたがって、graphql-codegen 起動方法は、`npm run codegen` になります。\n\n<br />\n\nその前に、このままの場合、  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Unable to find template plugin matching 'typescript-react-query'</span>  \nのようにエラーになります。  \nplugins に指定したパッケージを `--save-dev` でインストールします。\n\n```shellsession\n$ npm install --save-dev @graphql-codegen/typescript-operations\n$ npm install --save-dev @graphql-codegen/typescript-react-query\n$ npm install --save-dev @graphql-codegen/typescript-resolvers\n$ mkdir generated\n$ npm install\n$ npm run codegen\n```\n\n<br />\n\n# Next.js Apllo Server 実装\n\n`http://localhost:3000/api/graphql` を Apllo Server(GraphQL エンドポイント)として仕立てていきます。  \nApllo Server v4 未満の情報しか無く、ここが一番困りましたが、  \n<a href=\"https://github.com/apollo-server-integrations/apollo-server-integration-next\" target=\"_blank\">@as-integrations/next(`https://github.com/apollo-server-integrations/apollo-server-integration-next`)</a> というドンピシャな npm を見つけました。\n\n<br />\n\n`startServerAndCreateNextHandler.ts` から `startServerAndCreateNextHandler` を import すれば良いため、今回は丸ごと拝借することにします。\n\n```shellsession\n$ vi pages/api/graphql/startServerAndCreateNextHandler.ts\n```\n\n```ts:pages/api/graphql/startServerAndCreateNextHandler.ts\nimport {\n  ApolloServer,\n  BaseContext,\n  ContextFunction,\n  HeaderMap,\n} from \"@apollo/server\";\nimport type { WithRequired } from \"@apollo/utils.withrequired\";\nimport { NextApiHandler } from \"next\";\nimport { parse } from \"url\";\n\ninterface Options<Context extends BaseContext> {\n  context?: ContextFunction<Parameters<NextApiHandler>, Context>;\n}\n\nconst defaultContext: ContextFunction<[], any> = async () => ({});\n\nfunction startServerAndCreateNextHandler(\n  server: ApolloServer<BaseContext>,\n  options?: Options<BaseContext>\n): NextApiHandler;\nfunction startServerAndCreateNextHandler<Context extends BaseContext>(\n  server: ApolloServer<Context>,\n  options: WithRequired<Options<Context>, \"context\">\n): NextApiHandler;\nfunction startServerAndCreateNextHandler<Context extends BaseContext>(\n  server: ApolloServer<Context>,\n  options?: Options<Context>\n) {\n  server.startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests();\n\n  const contextFunction = options?.context || defaultContext;\n\n  const handler: NextApiHandler = async (req, res) => {\n    const headers = new HeaderMap();\n\n    for (const [key, value] of Object.entries(req.headers)) {\n      if (typeof value === \"string\") {\n        headers.set(key, value);\n      }\n    }\n\n    const httpGraphQLResponse = await server.executeHTTPGraphQLRequest({\n      context: () => contextFunction(req, res),\n      httpGraphQLRequest: {\n        body: req.body,\n        headers,\n        method: req.method || \"POST\",\n        search: req.url ? parse(req.url).search || \"\" : \"\",\n      },\n    });\n\n    for (const [key, value] of httpGraphQLResponse.headers) {\n      res.setHeader(key, value);\n    }\n\n    res.statusCode = httpGraphQLResponse.status || 200;\n\n    if (httpGraphQLResponse.body.kind === \"complete\") {\n      res.send(httpGraphQLResponse.body.string);\n    } else {\n      for await (const chunk of httpGraphQLResponse.body.asyncIterator) {\n        res.write(chunk);\n      }\n\n      res.end();\n    }\n  };\n\n  return handler;\n}\n\nexport { startServerAndCreateNextHandler };\n```\n\n<br />\n\nこのままの場合、  \n<span style=\"color: #e70500;background-color: #ffebe7;\">Type 'HeaderMap' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher.ts(2802)</span>  \nエラーになります。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/image3.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/image3.png\" alt=\"tsエラー\" width=\"1200\" height=\"273\" loading=\"lazy\"></a>\n\n<br />\n\n```shellsession\n$ vi tsconfig.json\n```\n\ntsconfig.json の compilerOptions セクションに\n\n```json:tsconfig.json\n    \"downlevelIteration\": true,\n```\n\nを追加します。\n\n<blockquote class=\"info\">\n<p>【 \"downlevelIteration\": true 】</p>\n<p>for..of 構文などの ES6 から追加されたイテレーション系の記法をコンパイルします。</p>\n</blockquote>\n\n<br />\n\n`pages/api/graphql/index.ts` を作成します。  \nこれは、`http://localhost:3000/api/graphql` で呼ばれたときに、最初に呼ばれる部分です。\n\n```shellsession\n$ vi pages/api/graphql/index.ts\n```\n\n```ts:pages/api/graphql/index.ts\nimport { typeDefs } from \"./schema\";\nimport { dataSources } from \"./data-sources\";\nimport { resolvers } from \"./resolvers\";\nimport { ApolloServer } from \"@apollo/server\";\nimport { startServerAndCreateNextHandler } from \"./startServerAndCreateNextHandler\";\n\nconst apolloServer = new ApolloServer({ typeDefs, resolvers });\nexport default startServerAndCreateNextHandler(apolloServer, {\n  context: async (req, res) => ({ req, res, dataSources: dataSources() }),\n});\n```\n\n<br />\n\n`const apolloServer = new ApolloServer({ (コンフィグ), (resolvers) });`  \n`export default startServerAndCreateNextHandler(apolloServer);`  \nでOKですが、今回は、外部からの情報(2つの REST API)を使うため、options の context に dataSources を適用しています。  \n<span style=\"color: red;\"><strong>Apollo Server v4 により、以下の方法ではなくなりましたので、注意が必要です。</strong></span>\n\n```ts\nconst server = new ApolloServer({\n  typeDefs,\n  resolvers,\n  dataSources: () => ({\n    givefood: new GiveFoodDataSource(),\n  }),\n});\n```\n\n<br />\n\nRESTDataSource を拡張して、REST API でデータを操作するクラスを2つ作成します。\n\n<br />\n\n名前の取得、変更ができる NamesAPI です。\n\n```shellsession\n$ vi pages/api/graphql/names-api.ts\n```\n\n```ts:pages/api/graphql/names-api.ts\nimport { RESTDataSource } from \"@apollo/datasource-rest\";\n\nexport type Name = {\n  name: string;\n};\n\ntype MutationResponse = { status: string; data: Name };\n\nexport default class NamesAPI extends RESTDataSource {\n  constructor() {\n    super();\n    this.baseURL = process.env.NAMES_REST_URL || \"http://localhost:4000/\";\n  }\n\n  async getName(nameId: number) {\n    return this.get<Name>(`names/${nameId}`);\n  }\n\n  async getNames() {\n    return this.get<Name[]>(`names`);\n  }\n\n  async postName(name: Name) {\n    return this.post<MutationResponse>(`names`, { body: name }).then(\n      (resp) => resp.data\n    );\n  }\n\n  async putName(nameId: number, name: Name) {\n    return this.put<MutationResponse>(`names/${nameId}`, {\n      body: name,\n    }).then((resp) => resp.data);\n  }\n}\n```\n\n<br />\n\n住所の取得ができる AddressesAPI です。(データの変更はできない仕様とします。)\n\n```shellsession\n$ vi pages/api/graphql/addresses-api.ts\n```\n\n```ts:pages/api/graphql/addresses-api.ts\nimport { RESTDataSource } from \"@apollo/datasource-rest\";\n\nexport type Address = {\n  address: string;\n};\n\nexport default class AddressesAPI extends RESTDataSource {\n  constructor() {\n    super();\n    this.baseURL = process.env.ADDRESSES_REST_URL || \"http://localhost:5000/\";\n  }\n\n  async getAddress(addressId: number) {\n    return this.get<Address>(`addresses/${addressId}`);\n  }\n\n  async getAddresses() {\n    return this.get<Address[]>(`addresses`);\n  }\n}\n```\n\n<br />\n\n2つのデータソースを束ねます。\n\n```shellsession\n$ vi pages/api/graphql/data-sources.ts\n```\n\n```ts:pages/api/graphql/data-sources.ts\nimport NamesAPI from \"./names-api\";\nimport AddressesAPI from \"./addresses-api\";\n\nexport const dataSources = () => ({\n  namesAPI: new NamesAPI(),\n  addressesAPI: new AddressesAPI(),\n});\n\nexport type DataSources = ReturnType<typeof dataSources>;\n```\n\n<br />\n\nGraphQL リゾルバを定義します。\n\n<blockquote class=\"info\">\n<p>【 リゾルバ(Resolver) 】</p>\n<p>特定のフィールドのデータを返す関数(メソッド)です。例えば、nameフィールドがクエリに含まれていた場合、何を返すか定義する場合、</p>\n<p><code>name: 処理内容あるいは値そのもの</code></p>\n<p>です。今回の場合、全ての操作において、第三引数 context(= GraphQL operation の resolver 全体で共有されるオブジェクト。)で外部の REST API を使う共通処理を受け取っています。</p>\n</blockquote>\n\n```shellsession\n$ vi pages/api/graphql/resolvers.ts\n```\n\n```ts:pages/api/graphql/resolvers.ts\nimport { Resolvers } from \"../../../generated/resolvers\";\nimport { DataSources } from \"./data-sources\";\n\nexport type Context = { dataSources: DataSources };\n\nexport const resolvers: Resolvers = {\n  Query: {\n    // context = GraphQL operation の resolver 全体で共有されるオブジェクト。\n    // context = 外部のデータ取得処理 = data-sources.ts に書かれている。\n    // { dataSources } = context.dataSources と同意。\n    // context は resolver の第三引数に渡され、アクセスできる。\n    // その処理内容(クラス名.メソッド)は、names-api.ts、address-api.ts にある。\n    name: async (_parent, { id }, { dataSources }) =>\n      dataSources.namesAPI.getName(parseInt(id)),\n    names: async (_parent, _args, { dataSources }) =>\n      dataSources.namesAPI.getNames(),\n    address: async (_parent, { id }, { dataSources }) =>\n      dataSources.addressesAPI.getAddress(parseInt(id)),\n    addresses: async (_parent, _args, { dataSources }) =>\n      dataSources.addressesAPI.getAddresses(),\n  },\n  Mutation: {\n    addName: async (_parent, { input }, { dataSources }) =>\n      dataSources.namesAPI.postName({ ...input }),\n    updateName: async (_parent, { id, input }, { dataSources }) =>\n      dataSources.namesAPI.putName(parseInt(id), { ...input }),\n  },\n};\n```\n\n完成です!\n\n<br />\n\n# 動作確認その1\n\n```shellsession\n$ npm run dev\n```\n\nで、`http://localhost:3000` にアクセスします。\n\n当然ですが、  \n`pages/_app.tsx`  \n`pages/index.tsx`  \nを何も変更していないため、  \nNext.js デフォルトのフロントエンドの画面が表示されます。\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/image4.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/image4.png\" alt=\"デフォルトのフロントエンドの画面\" width=\"879\" height=\"662\" loading=\"lazy\"></a>\n\n<br />\n\n`http://localhost:3000/api/graphql` にアクセスします。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/image5.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/image5.png\" alt=\"Apollo Server 表示\" width=\"1173\" height=\"715\" loading=\"lazy\"></a>\n\nApollo Server が表示されました!\n\n<br />\n\nクエリを実行します。\n\n```graphql\nquery getNamesAndAddresses {\n  names {\n    name\n  }\n  addresses {\n    address\n  }\n}\n```\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/image6.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/image6.png\" alt=\"接続エラー\" width=\"1174\" height=\"719\" loading=\"lazy\"></a>\n\n接続エラーになりました!\n\n<br />\n\nそれもそのはず、  \nNamesAPI: `http://localhost:4000`  \nAddressesAPI: `http://localhost:5000`  \nをまだ作っていません...。\n\n<br />\n\n作ります。\n\n<br />\n\n# REST API サーバー作成\n\nnode(ts-node)の express で REST API サーバーをサクッと作成します。\n\n<br />\n\n## NamesAPI\n\n<blockquote class=\"info\">\n<p>プロジェクト初期化は、何も考えずに、TypeScript 公式サイト(https://typescript-jp.gitbook.io/deep-dive/nodejs)Node.js & TypeScript のプロジェクト作成 の手順を実施しています。</p>\n</blockquote>\n\n```shellsession\n$ mkdir rest-api-names\n$ cd rest-api-names\n$ npm init -y\n$ npm install typescript --save-dev\n$ npx tsc --init --rootDir src --outDir lib --esModuleInterop --resolveJsonModule --lib es6,dom --module commonjs\n$ npm install ts-node body-parser express @types/express\n$ vi package.json\n```\n\n`\"start\": \"ts-node src/index.ts\",` を追加します。\n\n```json:package.json\n  \"scripts\": {\n    \"start\": \"ts-node src/index.ts\",\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n```\n\n<br />\n\nNamesAPI REST API サーバープログラムを作成します。\n\n<br />\n\nここで、この API の仕様は、4000 番ポートで起動し、以下の実装内容とします。\n\n| method | path       | 動作内容                                                                  |\n| ------ | ---------- | ------------------------------------------------------------------------- |\n| GET    | /names     | 保持している全ての名前を返す。                                            |\n| GET    | /name/[id] | id(何番目か)に該当する名前を返す。                                      |\n| POST   | /name      | パラメータの name を追加する。                                            |\n| PUT    | /name      | パラメータの id(何番目か)に該当する名前をパラメータの name に変更する。 |\n\n<blockquote class=\"info\">\n<p>データの初期値は、この後作成する <code>names-data.json</code> です。</p>\n<p><span style=\"color:red;\"><strong>POSTで追加、PUTで追加可能ですが、jsonを書き換えるのではなく、メモリを書き換えています。サーバーを停止すると、変更内容は失われます。</strong></span></p>\n</blockquote>\n\n<br />\n\n```shellsession\n$ mkdir src\n$ vi src/index.ts\n```\n\n```ts:src/index.ts\nimport express from \"express\";\nimport bodyParser from \"body-parser\";\nimport names from \"./names-data.json\";\n\ntype Name = typeof names[0];\n\nconst port = 4000;\nconst app = express();\n\nlet dataStore = [...names];\n\napp.use(bodyParser.json());\n\napp.get<{}, Name[]>(\"/names\", (req: any, res: any) => {\n  res.json(dataStore);\n});\n\napp.get<{ nameId: string }, Name>(\"/names/:nameId\", (req: any, res: any) => {\n  res.json(dataStore[parseInt(req.params.nameId)]);\n});\n\napp.post<{}, { status: string; data: Name }, Name>(\n  \"/names\",\n  (req: any, res: any) => {\n    dataStore.push(req.body);\n    const newNameId = dataStore.length - 1;\n    res.json({ status: \"ok\", data: dataStore[newNameId] });\n  }\n);\n\napp.put<{ nameId: string }, { status: string; data: Name }, Name>(\n  \"/names/:nameId\",\n  (req: any, res: any) => {\n    const nameIdToChange = parseInt(req.params.nameId);\n    dataStore = dataStore.map((name, nameId) => {\n      if (nameId === nameIdToChange) {\n        return req.body;\n      }\n      return name;\n    });\n    res.json({ status: \"ok\", data: dataStore[nameIdToChange] });\n  }\n);\n\napp.listen(port, () => {\n  console.log(`API server started at http://localhost:${port}`);\n});\n```\n\n<br />\n\nNamesAPI REST API サーバーのデータ初期値を作成します。\n\n```shellsession\n$ vi src/names-data.json\n```\n\n```json:src/names-data.json\n[\n  {\n    \"name\": \"John Titor\"\n  },\n  {\n    \"name\": \"Werner Karl Heisenberg\"\n  },\n  {\n    \"name\": \"Walter White\"\n  }\n]\n```\n\n<br />\n\n起動します。\n\n```shellsession\n$ npm start\n```\n\n<br />\n\n## AddressesAPI\n\n```shellsession\n$ mkdir rest-api-addresses\n$ cd rest-api-addresses\n$ npm init -y\n$ npm install typescript --save-dev\n$ npx tsc --init --rootDir src --outDir lib --esModuleInterop --resolveJsonModule --lib es6,dom --module commonjs\n$ npm install ts-node body-parser express @types/express\n$ vi package.json\n```\n\n`\"start\": \"ts-node src/index.ts\",` を追加します。\n\n```json:package.json\n  \"scripts\": {\n    \"start\": \"ts-node src/index.ts\",\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n```\n\n<br />\n\nAddressesAPI REST API サーバープログラムを作成します。\n\n<br />\n\nここで、この API の仕様は、5000 番ポートで起動し、以下の仕様とします。\n\n| method | path          | 動作内容                             |\n| ------ | ------------- | ------------------------------------ |\n| GET    | /addresses    | 保持している全ての住所を返す。       |\n| GET    | /address/[id] | id(何番目か)に該当する住所を返す。 |\n\n<blockquote class=\"info\">\n<p>データの初期値は、この後作成する <code>addresses-data.json</code> です。</p>\n<p>AddressesAPI REST API の方は、データの更新は無しとします。</p>\n</blockquote>\n\n<br />\n\n```shellsession\n$ mkdir src\n$ vi src/index.ts\n```\n\n```ts:src/index.ts\nimport express from \"express\";\nimport bodyParser from \"body-parser\";\nimport addresses from \"./addresses-data.json\";\n\ntype Address = typeof addresses[0];\n\nconst port = 5000;\nconst app = express();\n\nlet dataStore = [...addresses];\n\napp.use(bodyParser.json());\n\napp.get<{}, Address[]>(\"/addresses\", (req: any, res: any) => {\n  res.json(dataStore);\n});\n\napp.get<{ addressId: string }, Address>(\n  \"/addresses/:addressId\",\n  (req: any, res: any) => {\n    res.json(dataStore[parseInt(req.params.addressId)]);\n  }\n);\n\napp.listen(port, () => {\n  console.log(`API server started at http://localhost:${port}`);\n});\n```\n\n<br />\n\nAddresses REST API サーバーのデータ初期値を作成します。\n\n```shellsession\n$ vi src/addresses-data.json\n```\n\n```json:src/addresses-data.json\n[\n  {\n    \"address\": \"568 Blueberry Blvd, Indian Island, ME 04468\"\n  },\n  {\n    \"address\": \"PO Box 230, Skan Falls, NY 13153\"\n  },\n  {\n    \"address\": \"224 Blueberry Ct #837, Harkeyville, TX 76877\"\n  }\n]\n```\n\n<br />\n\n起動します。\n\n```shellsession\n$ npm start\n```\n\n<br />\n\n# 動作確認その2\n\n再び、`http://localhost:3000/api/graphql` でクエリを実行してみます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/image7.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/image7.png\" alt=\"再びクエリを実行\" width=\"1200\" height=\"567\" loading=\"lazy\"></a>\n\nヨシ!\n\n<br />\n\n名前を追加してみます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/image8.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/image8.png\" alt=\"名前を追加\" width=\"1200\" height=\"563\" loading=\"lazy\"></a>\n\nヨシ!\n\n<br />\n\n一番目の名前を変更してみます。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/image9.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/image9.png\" alt=\"一番目の名前を変更\" width=\"1200\" height=\"534\" loading=\"lazy\"></a>\n\nヨシ!\n\n<br />\n\nもう一度全体を確認します。\n\n<a href=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/image10.png\" target=\"_blank\" rel=\"nofollow noopener\"><img src=\"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/image10.png\" alt=\"もう一度全体を確認\" width=\"1200\" height=\"531\" loading=\"lazy\"></a>\n\nヨシッ!!\n\n<br />\n\n続きは、こちらへ(GraphQL フロントエンドの実装を行います。)  \n↓  \n<a href=\"https://itc-engineering-blog.netlify.app/blogs/nextjs-graphql-react-query\" target=\"_blank\">Next.js, graphql-codegen, TanStack Query, Apollo Server v4 で簡易 BFF 作成(2/2)</a>\n\n<br />\n","description":"【サーバー側作業編1/2】Next.js v13、graphql-codegen 2.16.1、React v18、TypeScript、TanStack Query v4(React Query)、Apollo Server v4 で簡易 BFF を作成しました。","reflect_updatedAt":false,"reflect_revisedAt":false,"seo_images":[{"id":"be7uqgi8c","createdAt":"2022-12-30T12:01:47.607Z","updatedAt":"2022-12-30T12:01:47.607Z","publishedAt":"2022-12-30T12:01:47.607Z","revisedAt":"2022-12-30T12:01:47.607Z","url":"https://res.cloudinary.com/dt8zu6zzd/image/upload/blog/nextjs-graphql-bff-apollo-server/ITC_Engineering_Blog.png","alt":"Next.js, graphql-codegen, React Query, Apollo Server v4 で簡易BFF作成(1/2)","width":1200,"height":630}],"seo_authors":[]}],"totalCount":42,"offset":0,"limit":100}