狛ログ

2022年10月29日土曜日

Mavenビルドの「Error code 501, HTTPS Required」エラーに対応する。


オフィス狛 技術部のJoeです。
先日、担当しているJavaのプロジェクトでMavenのビルドを行ったところ、下記のエラーが発生しました。

  1. [ERROR] Failed to execute goal on project web: Could not resolve dependencies for project [プロジェクト]: Failed to collect dependencies at [アーティファクトID]: Failed to read artifact descriptor for [アーティファクトID]: Could not transfer artifact org.springframework.boot:spring-boot-dependencies:pom:1.5.4.RELEASE from/to central (http://repo1.maven.org/maven2/): Failed to transfer http://repo1.maven.org/maven2/org/springframework/boot/spring-boot-dependencies/1.5.4.RELEASE/spring-boot-dependencies-1.5.4.RELEASE.pom. Error code 501, HTTPS Required

Maven Central リポジトリからアーティファクトをダウンロードできずに、「501」でエラーとなっています。

原因は、2020年1月15日から、Maven の Central リポジトリ へのHTTP経由による接続のサポートが終了し、HTTPS経由での接続が必須となりました。(ちょっと古い情報ですが)
そのため、このままですと、Central リポジトリ への接続が拒否され、ビルドが失敗します。

解決方法を調査してみると、いくつか対応策が出てきますが、私が解決できた方法をご紹介します。

【環境】
・Spring Tool Suite(Windows):4.15.3
・Maven:3.8.4

※環境の状態によっては、下記のエラーが発生する場合もありますが、同じ対応で解決できました
  1. [ERROR] Failed to execute goal on project web: Could not resolve dependencies for project [プロジェクト]: Failed to collect dependencies at [アーティファクトID]: Failed to read artifact descriptor for [アーティファクトID]: org.springframework.boot:spring-boot-dependencies:pom:1.5.4.RELEASE was not found in https://oss.sonatype.org/content/repositories/snapshots during a previous attempt. This failure was cached in the local repository and resolution is not reattempted until the update interval of sonatype-snapshots has elapsed or updates are forced

① エラーとなったアーティファクトのバージョンを最新版に上げる

私の利用していたアーティファクト「doma-spring-boot-starter」は、バージョンを最新版(1.1.1 → 1.6.0)にすることでエラーが解消し、アーティファクトをダウンロードできました。
※アーティファクトによっては、最新バージョンとしても今回の事象が解消されていない可能性もあるかと思います

バージョンの上げ方は、Central リポジトリで、ご利用のアーティファクトを検索、最新バージョンを確認いただき、pom.xmlファイルで、対象のアーティファクトの「<version>タグ」で指定下さい。

② Maven Central リポジトリのURLを指定する

①でエラーが解消されない場合や、事情によりバージョンを変更できない場合は、pom.xmlファイルに、下記のとおり、Maven Central リポジトリのURLを明示的に指定することで、エラーを解消することが出来ました。
  1. <repositories>
  2. <repository>
  3. <id>maven</id>
  4. <name>Maven Central</name>
  5. <url>https://repo.maven.apache.org/maven2/</url>
  6. <snapshots>
  7. <enabled>false</enabled>
  8. </snapshots>
  9. </repository>
  10. </repositories>

※調査してみると、「<id>タグ」と「<name>タグ」には、下記を指定することで解消できるとの情報もありましたが、私の環境ではエラーを解消できませんでした。
  1. <id>central</id>
  2. <name>Central Repository</name>


①、②でpom.xmlファイルを編集し、ビルドしてもエラーが解消されない場合は、プロジェクトの更新(プロジェクトを右クリック > Maven > プロジェクトの更新)し、再度ビルドすることもお試しください。

以上です。
同じエラーが発生した場合に、お役に立てば幸いです。

2022年10月27日木曜日

【Angular】画面遷移前に確認ダイアログ風のポップアップを表示する。

オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。

今回は画面遷移前に表示する下図のような確認ダイアログ風のポップアップ(以降、確認ポップアップ)を作成していこうと思います。

1からこのような仕組みを作るのは大変なので、「Simple Modal Module」(ngx-simple-modal)というプラグインを使いたいと思います。

※「Simple Modal Module」のインストールや使い方の細かい説明は、弊社メンバーの記事(【Angular・【Simple Modal Module】モーダルを手軽に実装する。)をご覧ください。

当記事では、以前の記事で使用した「アカウント登録機能」に実装する形で、実践的な説明をしようと思います。
と言うことで、是非、以前の記事も参照ください

参照1:【Angular】独自エラーチェック(カスタムバリデーション)を作成する。
参照2:【Angular】コンポーネントの設計(画面ごとの設計)について。
参照3:【Angular】エラーメッセージの管理について考える。

(1)どのコンポーネントでポップアップを呼び出すべきか

まず、どのコンポーネントでポップアップを呼び出すべきか考えてみます。
親(Container)なのか、子(Presentation)なのか・・・・
以前の記事で、こんな定義をしたかと思います。

「Container」として分類するもの
  • 画画面の状態保持に関すること(NgRxのStore操作など)
  • 画面に入力した値の業務チェック(API呼び出しが必要なもの、など)
  • API(バックエンド処理)の呼び出し
  • 画面遷移

「Presentation」として分類するもの
  • 画面表示、及び表示内容の制御(エラー時など)
  • 画面に入力した値のバリデーションチェック
  • 画面に入力した値の関連チェック、及び業務チェック


という事で、ちょっと悩みどころではあるのですが、呼び出しは「子(Presentation)」の方が良いと思います。
確認ポップアップの「OKボタン=画面遷移する」、「キャンセルボタン=画面遷移しない」と考えると、確認ポップアップとしての動きは、あくまで「画面遷移するかどうかの関連チェック」に過ぎない、というのが理由です。
他にも、「キャンセルを押された時に画面の表示を変えたい」という要件があった場合、確認ポップアップを親(Container)で呼び出すと、かなり複雑になってしまうから、というのも理由の1つです。(今回はそのような要件はないですが)

(2)確認ポップアップの作成

呼び出す場所は決まりましたので、呼び出す「確認ポップアップ」用のコンポーネントを作成しようと思います。
弊社のプロジェクトでは、「app」配下に「shared」と言うディレクトリを作り、その中にプロジェクトで共通的に使うものを配置しています。

今回は、以下のように作成します。
  1. src/
  2. app/
  3. shared/
  4. modal/
  5. containers/
  6. confirmation-dialog/
  7. confirmation-dialog.component.html
  8. confirmation-dialog.component.ts

では、それぞれ実装を見ていきましょう。まずは、テンプレート(View)から。
[confirmation-dialog.component.html]
  1. <div class="message">
  2. <div class="modal-dialog modal-content">
  3. <div class="modal-header">
  4. <div class="modal-title"></div>
  5. <button type="button" class="close" aria-label="閉じる" (click)="onClickCancel()">
  6. <span aria-hidden="true">×</span></button>
  7. </div>
  8. <div class="modal-body">
  9. <p class="text-center">{{ message }}</p>
  10. </div>
  11. <div class="modal-footer p-0">
  12. <button class="btn btn2" (click)="onClickCancel()">キャンセル</button>
  13. <button class="btn btn1 btn-block" type="submit" (click)="onClickOk()">OK</button>
  14. </div>
  15. </div>
  16. </div>
特筆すべきところは特に無いですが、このコンポーネントを汎用的に利用する為に、表示するメッセージは「{{ message }}」で変数を展開する形にしています。

続いて、コンポーネントの実装です。
[rconfirmation-dialog.component.ts]
  1. import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
  2. import { SimpleModalComponent } from 'ngx-simple-modal';
  3.  
  4. export interface ConfirmModel {
  5. message: string;
  6. }
  7.  
  8. @Component({
  9. selector: 'koma-confirmation-dialog',
  10. templateUrl: './confirmation-dialog.component.html',
  11. changeDetection: ChangeDetectionStrategy.OnPush,
  12. })
  13. export class ConfirmationDialogComponent
  14. extends SimpleModalComponent<ConfirmModel, boolean>
  15. implements ConfirmModel, OnInit {
  16. message: string;
  17.  
  18. constructor() {
  19. super();
  20. }
  21.  
  22. ngOnInit() {}
  23.  
  24. onClickCancel() {
  25. this.result = false;
  26. this.close();
  27. }
  28.  
  29. onClickOk() {
  30. this.result = true;
  31. this.close();
  32. }
  33. }

少し詳細を説明しています。
まず、下記の記載でポップアップに表示するメッセージを呼び出し元で設定出来るようにしています。
  1. export interface ConfirmModel {
  2. message: string;
  3. }

あと重要なのは、「ngx-simple-modal」のクラス(SimpleModalComponent)を継承し、先程作った「ConfirmModel」を実装する、と言う事です。
  1. export class ConfirmationDialogComponent
  2. extends SimpleModalComponent<ConfirmModel, boolean>
  3. implements ConfirmModel, OnInit, OnDestroy {

「OKが押されたか、キャンセルが押されたか」は、「result」にTrue、Falseを設定することで、呼び出し側へ知らせます。
  1. onClickCancel() {
  2. this.result = false;
  3. this.close();
  4. }
  5.  
  6. onClickOk() {
  7. this.result = true;
  8. this.close();
  9. }

これで確認ポップアップが作成できました。次はこの確認ポップアップを呼び出してみましょう。

※モジュール(NgModule)ファイルの記載は省略していますので、適宜、追加したコンポーネントをモジュールでdeclarationsしておいてください。
※モジュール(NgModule)ファイルでの「SimpleModalModule(ngx-simple-modal)」のインポートについてですが、共通的に使用されることを考えると、「app.module.ts」などでインポートした方が良いと思います。

(3)確認ポップアップの呼び出し

では早速、先程作成した確認ポップアップの呼び出しを実装して行きます。
まずは、呼び出し側の子のテンプレート(View)を実装します。

[register.component.html](form部分のみ抜粋)
  1. <form [formGroup]="formRegister" (ngSubmit)="onSubmit()" (keydown.enter)="$event.preventDefault()">
  2. <div class="form-input">
  3. <div>
  4. <label>携帯電話番号<span>必須</span></label>
  5. <input formControlName="mobilePhoneNumber" type="tel" inputmode="numeric" class="form-control" placeholder="携帯電話番号を入力"
  6. [ngClass]="{'alert-danger' : v.mobilePhoneNumberInvalid}" autofocus>
  7. <ng-container *ngIf="v.mobilePhoneNumberInvalid">
  8. <p *ngIf="v.mobilePhoneNumberHasErrorRequired" class="error-message">{{ message('msg_error_field_required', '携帯電話番号') }}</p>
  9. <p *ngIf="v.mobilePhoneNumberHasErrorMaxLength" class="error-message">{{ message('msg_error_field_max', '携帯電話番号', mobilePhoneNumberMaxLength) }}</p>
  10. <p *ngIf="v.mobilePhoneNumberHasErrorFormat" class="error-message">{{ message('msg_error_cellphone_number') }}</p>
  11. </ng-container>
  12. </div>
  13. <div>
  14. <label>お名前</label>
  15. <input formControlName="name" type="text" class="form-control" placeholder="お名前を入力(任意)"
  16. [ngClass]="{'alert-danger' : v.nameInvalid}">
  17. <ng-container *ngIf="v.nameInvalid">
  18. <p class="error-message">{{ message('msg_error_field_max', 'お名前', nameMaxLength)}}</p>
  19. </ng-container>
  20. </div>
  21. </div>
  22. <footer class="footer fixed-bottom d-flex">
  23. <button type="button" (click)="onClickPrev()">戻る</button>
  24. <button [disabled]="formRegister.invalid" type="submit">確認</button>
  25. </footer>
  26. </form>

特筆すべき部分は特になく、単純に下記の部分を追加しているだけです。
  1. <button type="button" (click)="onClickPrev()">戻る</button>
これは単純ですね、「戻る」ボタンを追加しただけ、になります。

では、次は子コンポーネントに戻るボタンを押した時のアクションを実装して行きます。
[register.component.ts]
  1. import { Component, OnInit, Input, Output, OnDestroy, EventEmitter } from '@angular/core';
  2. import { FormBuilder, FormGroup, Validators } from '@angular/forms';
  3. import { Observable, Subscription } from 'rxjs';
  4. import { CustomValidators } from '@app/shared/validator/custom-validators';
  5. import { RegisterValidator } from './register.validator';
  6. import { AccountViewSaveModel } from '../../models/account';
  7. import { getMessage } from '@app/shared/message/error-messages';
  8. import { SimpleModalService } from 'ngx-simple-modal';
  9. import { ConfirmationDialogComponent } from '@app/shared/modal/containers/confirmation-dialog/confirmation-dialog.component';
  10.  
  11. @Component({
  12. selector: 'koma-register',
  13. templateUrl: './register.component.html',
  14. })
  15. export class RegisterComponent implements OnInit, OnDestroy {
  16. // Input・Outputの定義。
  17. @Input() accountViewSave$: Observable<AccountViewSaveModel>;
  18. @Output() formBack = new EventEmitter();
  19. @Output() formSubmit = new EventEmitter<AccountViewSaveModel>();
  20.  
  21. mobilePhoneNumberMaxLength = 11;
  22. nameMaxLength = 20;
  23.  
  24. registerSubscription: Subscription = new Subscription();
  25.  
  26. formRegister: FormGroup = this.formBuilder.group({
  27. mobilePhoneNumber: [
  28. '',
  29. [
  30. Validators.required,
  31. Validators.maxLength(this.mobilePhoneNumberMaxLength),
  32. CustomValidators.mobilePhoneNumberValidator,
  33. ],
  34. ],
  35. name: ['', [Validators.maxLength(this.nameMaxLength)]],
  36. });
  37.  
  38. constructor(
  39. private formBuilder: FormBuilder,
  40. public v: RegisterValidator,
  41. private simpleModalService: SimpleModalService,
  42. ) {}
  43.  
  44. ngOnInit(): void {
  45. // バリデーション設定
  46. this.v.formGroup = this.formRegister;
  47.  
  48. this.registerSubscription.add(
  49. // 画面初期値設定
  50. this.accountViewSave$.subscribe(value => {
  51. if (value) {
  52. this.formRegister.controls.mobilePhoneNumber.setValue(
  53. value.mobilePhoneNumber,
  54. );
  55. this.formRegister.controls.name.setValue(value.name);
  56. }
  57. }),
  58. );
  59. }
  60.  
  61. ngOnDestroy(): void {
  62. // ダイアログ削除
  63. this.simpleModalService.removeAll();
  64. // サブスクリプション解除
  65. this.registerSubscription.unsubscribe();
  66. }
  67.  
  68. onClickPrev(): void {
  69. // 確認ダイアログの表示
  70. this.simpleModalService
  71. .addModal(ConfirmationDialogComponent, {
  72. message: getMessage('msg_confirm_screen_transition'),
  73. })
  74. .subscribe(result => {
  75. if (result) {
  76. this.formBack.emit();
  77. }
  78. });
  79. }
  80.  
  81. // 画面でSubmitが発生した時の処理
  82. onSubmit(): void {
  83. if (this.formRegister.valid) {
  84. // バリデーションエラーが発生していない場合
  85. const formModel = {
  86. mobilePhoneNumber: this.formRegister.controls.mobilePhoneNumber.value,
  87. name: this.formRegister.controls.name.value,
  88. } as AccountViewSaveModel;
  89. this.formSubmit.emit(formModel);
  90. }
  91. }
  92.  
  93. message(messageId: string, ...args: any[]): string {
  94. return getMessage(messageId, ...args);
  95. }
  96. }

中身を細かく見ていきましょう。
まずは、「ngx-simple-modalのSimpleModalService」と、先程作成した確認ポップアップをインポートしています。
  1. import { SimpleModalService } from 'ngx-simple-modal';
  2. import { ConfirmationDialogComponent } from '@app/shared/modal/containers/confirmation-dialog/confirmation-dialog.component';

戻るボタンを押された場合の画面遷移の制御は、親コンポーネント側で行うので、親コンポーネントへのイベント登録(定義)も行います。
  1. @Output() formBack = new EventEmitter();

そして、インポートした「SimpleModalService」はDIする必要があります。
  1. constructor(
  2. private formBuilder: FormBuilder,
  3. public v: RegisterValidator,
  4. private simpleModalService: SimpleModalService,
  5. ) {}

次に「戻るボタン」を押した時の処理です。ここで確認ポップアップを表示します。
  1. onClickPrev(): void {
  2. // 確認ダイアログの表示
  3. this.simpleModalService
  4. .addModal(ConfirmationDialogComponent, {
  5. message: getMessage('msg_confirm_screen_transition'),
  6. })
  7. .subscribe(result => {
  8. if (result) {
  9. this.formBack.emit();
  10. }
  11. });
  12. }

ざっくり説明すると、
  • SimpleModalServiceのaddModalに自作コンポーネントを指定して、さらに自作コンポーネントのmessageに、表示用の文言を設定している
  • ポップアップの戻りは、subscribeで受け取り設定する(resultにTrue、Falseが設定されて戻ってくる)
と言う感じですね。
Trueの場合(確認ポップアップで「OK」が押された場合)、親画面へイベントを委譲しています。

(4)親コンポーネントの実装

確認ポップアップの表示とは直接関係ないですが、親コンポーネントも実装しておきましょう。
まずはテンプレート(View)に、子のOutput(formBack)と親のイベント(onClickBack)の紐付けを追加します。

[account-register.component.html]
  1. <koma-register
  2. (formBack)="onClickBack()"
  3. (formSubmit)="onSubmit($event)"
  4. [accountViewSave$]="accountViewSave$"
  5. ></koma-register>

続いて、コンポーネント側の実装です。
[account-register.component.ts]
  1. import { Component, OnInit } from '@angular/core';
  2. import { Store, select } from '@ngrx/store';
  3. import { Router } from '@angular/router';
  4. import { AccountViewSaveModel } from '../../models/account';
  5. import * as fromAccount from '../../store/reducers';
  6. import * as AccountActions from '../../store/actions/account.actions';
  7.  
  8. @Component({
  9. selector: 'koma-account-register',
  10. templateUrl: './account-register.component.html',
  11. })
  12. export class AccountRegisterComponent implements OnInit {
  13.  
  14. // Storeから入力情報を取得する(確認画面から戻ってきた場合には値が入っている)
  15. // 取得値の型はObservableになるので、変数の末尾に「$」を付ける(お約束みたいなもの)
  16. accountViewSave$ = this.store.pipe(select(fromAccount.getDataRegisterPost));
  17.  
  18. constructor(public store: Store<fromAccount.State>, private router: Router) {}
  19.  
  20. ngOnInit(): void {}
  21.  
  22. onClickBack(): void {
  23. // ホーム画面に遷移する(戻る)
  24. this.router.navigateByUrl('/home/top');
  25. }
  26.  
  27. onSubmit(formModel: AccountViewSaveModel): void {
  28. // 入力情報をStoreに格納して(=引き継ぎ情報として)、遷移後の画面でも使えるようにする。
  29. this.store.dispatch(
  30. AccountActions.setRegisterPostData({ data: formModel }),
  31. );
  32. // 確認画面へ遷移する
  33. this.router.navigateByUrl('/account/register-confirm');
  34. }
  35. }

「NgRxのStore」などが使われていますが、ここは一旦スルーしてください。(いずれ、NgRxの実装方法も記事にしようと思います。)
ここでは、画面で入力された値はStore(と言う場所)に格納されている、ぐらいの理解でOKです。
(重要なのは、「データ保存方式」ではなく、あくまで「戻るボタンの制御」なので)

と言う事で、中身を見ていくと・・・・先程、子側と紐付けたイベント(onClickBack)が実装されていますね。
画面遷移するだけの処理ですが、URLがそのまま固定文字列で記載されているのが気になります。
こちらも今回の記事の本質では無いですが、URLの固定文字列も定数化しちゃいましょう。

(2)でも説明したように、「app」配下に「shared」と言うディレクトリを作り、その中にプロジェクトで共通的に使うものを配置します。
今回は、以下のように作成します。
  1. src/
  2. app/
  3. shared/
  4. constant/
  5. page-constants.ts

実装は下記のようになります。
[page-constants.ts]
  1. export namespace url {
  2. export const HOME_TOP = '/home/top';
  3. export const ACCOUNT_REGISTER_CONFIRM = '/account/register-confirm';
  4. }

では、先程の固定文字列を定数に変更してみます。
下記が、最終的な親のコンポーネントとなります。
[account-register.component.ts]
  1. import { Component, OnInit } from '@angular/core';
  2. import { Store, select } from '@ngrx/store';
  3. import { Router } from '@angular/router';
  4. import * as PageConstants from '@app/shared/constant/page-constants';
  5. import { AccountViewSaveModel } from '../../models/account';
  6. import * as fromAccount from '../../store/reducers';
  7. import * as AccountActions from '../../store/actions/account.actions';
  8.  
  9. @Component({
  10. selector: 'koma-account-register',
  11. templateUrl: './account-register.component.html',
  12. })
  13. export class AccountRegisterComponent implements OnInit {
  14.  
  15. // Storeから入力情報を取得する(確認画面から戻ってきた場合には値が入っている)
  16. // 取得値の型はObservableになるので、変数の末尾に「$」を付ける(お約束みたいなもの)
  17. accountViewSave$ = this.store.pipe(select(fromAccount.getDataRegisterPost));
  18.  
  19. constructor(public store: Store<fromAccount.State>, private router: Router) {}
  20.  
  21. ngOnInit(): void {}
  22.  
  23. onClickBack(): void {
  24. // ホーム画面に遷移する(戻る)
  25. this.router.navigateByUrl(PageConstants.url.HOME_TOP);
  26. }
  27.  
  28. onSubmit(formModel: AccountViewSaveModel): void {
  29. // 入力情報をStoreに格納して(=引き継ぎ情報として)、遷移後の画面でも使えるようにする。
  30. this.store.dispatch(
  31. AccountActions.setRegisterPostData({ data: formModel }),
  32. );
  33. // 確認画面へ遷移する
  34. this.router.navigateByUrl(PageConstants.url.ACCOUNT_REGISTER_CONFIRM);
  35. }
  36. }

(5)メッセージを追加する

最後に確認ポップアップに送る為のメッセージ定義もしておきましょう。

[error-messages.ts]
  1. export const errorMessages: { [key: string]: string } = {
  2. msg_error_field_required: '{0}は必ず入力してください。',
  3. msg_error_field_max: '{0}は{1}文字以内で入力してください。',
  4. msg_confirm_screen_transition:
  5. '現在の入力中の情報は破棄されます。前の画面に戻りますか?',
  6. };
  7.  
  8. function formatMessage(msg: string, ...args: any[]): string {
  9. return msg.replace(/\{(\d+)\}/g, (m, k) => {
  10. return args[k];
  11. });
  12. }
  13.  
  14. export function getMessage(messageId: string, ...args: any[]): string {
  15. return formatMessage(errorMessages[messageId], ...args);
  16. }

これで、全ての実装が完了しました。
この応用(むしろ、今回の方が応用ですが)で、「アラート表示ポップアップ」や、「完了メッセージ表示ポップアップ」など、色々作れると思います。

ぜひ、色々カスタマイズしてみてください!


【AWS】プライベートサブネット内のLambda関数でKMSを利用する(NAT Gatewayとエンドポイントについて)


オフィス狛 技術部のJoeです。
担当のプロジェクトで、AWSのプライベートサブネット内のLambda関数で、環境変数の複合化を行うため、AWS KMSを利用する必要がありました。
インターネットゲートウェイとルーティングできないプライベートサブネット内のLambda関数が、VPC外のサービスであるAWS KMSを利用するためには、2つの方法があります。

方法と設定

1.NAT Gatewayを作成する


パブリックサブネットにNAT Gatewayを作成し、インターネット経由でKMSに接続します。

【NAT Gatewayの設定手順】
 ※NAT Gateway 以外は、既に構築済みの前提として、手順は省略させていただきます

① VPCのサービスから、NAT Gateway を作成します。
・名前:任意の名前を指定します。
・サブネット:NAT Gatewayを作成するパブリックサブネット を選択します。
・接続タイプ:「パブリック」を選択します。
・Elastic IP 割り当て ID:任意のElastic IP アドレスを選択します。
 ※「Elastic IP を割り当て」ボタンで、新たなElastic IP を割り当てることも可能です

② プライベートサブネットのルートテーブルのルートに、①で作成したNAT Gateway を指定します。
・送信先:「0.0.0.0/0」を指定します。
・ターゲット:①で作成したNAT GatewayのID(「nat-…」)を指定します。

■注意点
・Elastic IP アドレスは、AWS アカウントごとに各リージョンで 5 つのデフォルト制限がありますので、既に5つ割り当てている場合、制限の増加をリクエストする必要があります。


2.エンドポイントを作成する(PrivateLink)

KMS用のエンドポイント(インターフェイスエンドポイント)を作成し、プライベートリンクでKMSに接続します。

【エンドポイントの設定手順】
 ※エンドポイント以外は、既に構築済みの前提として、手順は省略させていただきます

① VPCのサービスからエンドポイントを作成します。
・名前タグ : 任意の名前を指定します。
・サービスカテゴリ : 「AWS のサービス」を選択します。
・サービス : 「com.amazonaws.ap-northeast-1.kms」を選択します。
・VPC : ご自身の環境の、VPC を選択します。
・DNS 名を有効化 : プライベート DNS 名を有効にする場合、チェックしてください。
・DNS レコードの IP タイプ : DNS 名を有効にした場合、IPv4 or IPv6 を選択ください。
・サブネット : ご自身の環境の、プライベートサブネット を選択します。
・IP アドレスタイプ : 選択したサブネットのIP アドレスタイプから、IPv4 or IPv6 を選択ください。
・セキュリティグループ : 任意のセキュリティグループを指定します。
・ポリシー : 今回は「フルアクセス」としますが、制限したい場合「カスタム」を選択し、ポリシーを設定します。

■注意点
・エンドポイントに設定するセキュリティグループは、インバウンドルールに「HTTPS」を許可する必要があります。


NAT Gatewayとエンドポイントのどちらを利用したほうが良いのか


ご利用に環境による場合もあるかもしれませんが、今回のように、新たにプライベートサブネット内のLambdaやEC2から、AWSのサービス(KMS、S3など)を利用する場合は、 下記の理由などから、エンドポイントを作成し、プライベートリンクで接続が良さそうです。

ただし、対象の AWS サービスで VPC エンドポイントが利用できない場合もありますので、こちらで事前にご確認ください。
AWS PrivateLink と統合できる AWS のサービス

1.セキュリティ

NAT Gatewayは、インターネット経由で接続しますが、プライベートリンクを利用すると、トラフィックをインターネットに公開することなく、AWS のサービスに接続できます。
そのため、ブルートフォース攻撃やDDos攻撃、その他の脅威に晒される危険が軽減されます。
また、セキュリティグループを関連付けることで、アクセス制御も可能です。

2.料金

料金については、AWS アーキテクチャ ブログを見ますと、エンドポイント(インターフェイスエンドポイント)は、「インターネット ゲートウェイへのトラフィックを回避し、NAT ゲートウェイ、NAT インスタンス、またはファイアウォールの維持に関連するコストが発生するのを回避することで、ネットワーク パスを最適化できます。」との記載があります。
また、NAT Gatewayと「1つ当たりの料金」と「処理データ 1 GB あたりの料金」を比較すると、料金は約6分の1で、AWSのコストを削減できる可能性があります。
NAT Gatewayの料金
エンドポイントの料金

3.管理

管理については、エンドポイントは設定手順が少なく、構成も分かり易いため、個人的には管理し易いと思いました。


以上です。
今回は、インターフェイスエンドポイント(プライベートリンク)についてご紹介しましたが、エンドポイントは他にも「ゲートウェイロードバランサーのエンドポイント」、「ゲートウェイエンドポイント」がありますので、機会があればご紹介したいと思います。

,

2022年10月26日水曜日

【Laravel】created_atの日付フォーマットを変更する方法。

こんにちは、オフィス狛 技術部のmmm(むー)です。

以前、Laravelのプロジェクトで、日付のフォーマットを編集したかったのですが、レスポンスの項目名がcreated_at の場合、うまく変換できず少しはまりました。

その時、調べたことについて記事に残します。

前提条件

■Laravel バージョン
$ php artisan -V
Laravel Framework 8.27.0


■SQL Server バージョン
SELECT @@VERSION;
Microsoft SQL Server 2017 (RTM-CU30) (KB5013756) - 14.0.3451.2 (X64)  	Jun 22 2022 18:20:15  	Copyright (C) 2017 Microsoft Corporation 	Developer Edition (64-bit) on Linux (Ubuntu 18.04.6 LTS)


1. したかったこと

当初、SQLでフォーマットを指定して、created_at カラムの値をレスポンスしたかったのですが、期待した形の値が取得できませんでした。
※コードは必要な箇所以外、簡易化して記載します。
127は「ISO 8601 (タイム ゾーン Z)」となります。
class ConditionsController extends Controller
{
    public function index(Request $request)
    {

			$conditions = Condition::select(
			            DB::raw("CONVERT(VARCHAR, created_at, 127) as created_at")
			        )->get()->first();

			return response()->json($conditions);
    }
}

/*
■結果
{
    "created_at": "2022-09-07T02:48:46.947000Z"
}
*/

本来、「2022-09-07T11:48:46.947」のようなフォーマットになる想定でしたが、
created_atの値は「2022-09-07T02:48:46.947000Z」でした。

一方、下記は全く同じ書き方ですが、取得するカラム名が created_at ではない場合、期待通りにフォーマットを変換することができました。
class ConditionsController extends Controller
{
    public function index(Request $request)
    {

			$conditions = Condition::select(
			            DB::raw("CONVERT(VARCHAR, created_at, 127) as test") // 別名でtestを設定
			        )->get()->first();

			return response()->json($conditions);
    }
}

/*
■結果
{
    "test": "2022-09-07T11:48:46.947"
}
*/

created_atカラムに、testという別名を付けると、値は「2022-09-07T11:48:46.947」でした。

2. 原因と解決策

調査した結果、created_atupdated_atはデフォルトの場合、EloquentがCarbonインスタンスへ変換していることがわかりました。
Modelから返ってきた値をCarbonのフォーマットを使用して編集するように修正しました。
class ConditionsController extends Controller
{
    public function index(Request $request)
    {

			$conditions = Condition::select(
			            DB::raw("CONVERT(VARCHAR, created_at, 127) as created_at")
			        )->get()->first();
			$response['created_at'] = $conditions['created_at']->toIso8601String(); // 変換処理を追記

			return response()->json($response);
    }
}

/*
■結果
{
    "created_at": "2022-09-07T11:48:46+09:00"
}
*/

今回は toIso8601String() を使用しましたが、基本的なフォーマットは下記ページに用意されています。
参考:https://carbon.nesbot.com/docs/#api-conversion

3. 補足

今回はControllerでフォーマットの修正を行いましたが、Modelで下記のように、属性を指定してフォーマットを指定することもできます。
protected $casts = [
    'created_at' => 'datetime:Y/m/d',
];

さらに、Laravelで get...Attribute (...は加工したい項目名)と記載して、フォーマットを指定することも可能です。
public function getCreatedAtAttribute($value)
{
    return Carbon::parse($value)->format('Y/m/d');
}

上記は一例となります。Laravelでは様々な機能が用意されているので、もっといろんな方法があると思います。

参考になりましたら、幸いです。
,

【Laravel】プログレスバーが表示されるコマンド実行の処理を作成する方法。

こんにちは。オフィス狛 技術部のmmm(むー)です。

担当しているプロジェクトで、Laravelのコマンドラインで実行するバッチ処理がいくつかあるのですが、ユーザー数が日に日に増えていき実行時間が長くなっていきました。

普段は自動で実行しているので特に問題ないのですが、先日テストのため手動でバッチを実行しないといけないことがあり、

(この処理いつ終わるんだろう・・・・)

となってしまいました😖

処理を実行して放置すれば良いとしても、目安もわからず、都度確認するのは大変だと思いますので、今回はコマンド実行の処理とプログレスバーの作成方法をご紹介いたします。

前提条件

■Laravel バージョン
$ php artisan -V
Laravel Framework 8.27.0


1. コマンドを作成する

コマンドを作成します。今回は例として、ランキング作成の処理としました。
# php artisan make:command [コマンド名]

$ php artisan make:command CreateRankingCommand

自動で作成されたファイルを確認すると下記のようになっています。
// app/Console/Commands/CreateRankingCommand.php

<?php
namespace App\Console\Commands;

use Illuminate\Console\Command;

class CreateRankingCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'command:name';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        return 0;
    }
}

まず、2つの項目を設定します。
1. $signatureに、コマンド名を設定します。ここに設定した名前を使用して、コマンドラインで処理を実行することになります。
2. $description に、このコマンドの説明を記載します。

修正すると、以下のようになりました。
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'command:create_ranking'; // 修正しました

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'ランキングを作成します。'; // 修正しました
    

次に、コマンドが実行できるか確認します。

処理内容はhandle()内に本来記載しますが、初期状態から何も修正していないので、特にエラー等が表示されなければ問題ありません。
# php artisan command:[コマンド名]

$ php artisan command:create_ranking

また、以下のようにコマンドが作成できているか確認することもできます。
$ php artisan list | grep command:
  command:create_ranking                  ランキングを作成します。


2. プログレスバーを作成する

コマンドが実行できることは確認できたので、処理とプログレスバーを作成します。

処理内容は、handle()メソッドの中に記載します。
大変簡素ですが、今回はただループするだけの処理を作成します。
public function handle()
    {
        $count = 10;

        for ($i = 1; $i <= $count; $i++) {
            // 本来、ここで何か処理を行います
        }

        return 0;
    }

ここにプログレスバーを表示する処理を追記していきます。
コードの説明としては、下記①②③のコメントに記載した通りです。
public function handle()
    {
        $count = 10;

        // ①処理の総ステップ数を指定します
        $progressBar = $this->output->createProgressBar($count);

        for ($i = 1; $i <= $count; $i++) {
            // 本来、ここで何か処理を行います

            // ②プログレスバーの表示を1つ分進めます
            $progressBar->advance();
        }

        // ③プログレスバーの表示を完了状態にします
        $progressBar->finish();

        return 0;
    }

処理を実行すると下記のようになります。
※この処理は中身がないため、すぐ終わります。
$ php artisan command:create_ranking
10/10 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%


とても簡単に処理の進捗度を表示できるようになりました!

以上となります。
参考にして頂ければ幸いです。

,

2022年10月3日月曜日

【Python/Flask】AWSのALB(プロキシ)経由でWebシステムにアクセスしたらHTTPSがHTTPに書き換わって盛大にハマった話。

技術部のyuckieee(ゆっきー)です。
今回は、PythonのFlaskフレームワークを使用し、Webシステム構築をした際にハマった事象について、解決策と合わせて、ご紹介しようと思います。

■Webシステム概要

発生事象の説明をする前に、まずは今回開発を行ったWebシステムの概要について共有しておきます。超ザックリとした概要ではありますが、以下のような構成・仕様となっていました。

【システム構成】
・ALB(ロードバランサー)
・EC2(Web/APサーバ)
 - OS:RedHatLinux8.x(EC2)
 - Web/AP:Apache 2.4.xx(mod_wsgiでFlaskと連携)
 - 言語:Python 3.8
 - フレームワーク:Flask
・通信プロトコル
 - クライアント⇔ALB:HTTPS(TCP/443※)
  ※HTTPで接続された場合でもALB側でHTTPSに変換して、リクエスト自体は受け付ける
 - ALB⇔EC2:HTTP(TCP/80)

(構成イメージ)

【Webシステムの仕様】
 ・BASIC認証を使用して、ログイン認証を行う
 ・ログイン認証後、トップページでユーザ情報をセッションに格納して他ページで使用する
 ・他ページはユーザ情報必須のため、トップページ以外へのダイレクトアクセス※は非許可
  ※ブラウザにURLを直接入力したり、お気に入りからアクセスした場合など
 ・ダイレクトアクセス検知時は、トップページに強制遷移させ、必ずトップページ経由とさせる

(画面遷移イメージ)

■発生した事象

Webシステム開発が完了し、システム構成で説明した通りの通信経路となるよう、Webシステムにアクセスする際のURLを以下のように変更しました。(URLはイメージです)

【開発時】http://websystem.com/
※Webシステムのあるサーバに直接アクセスするために設定されたWebシステムのURL
  ↓
【開発完了後】https://alb.websystem.com/
※AWSのALBを介してアクセスするために設定されたWebシステムのURL

そして、動作確認をしようと【開発完了後】のURLにアクセスしたのですが、通常アクセス時のとおりトップページから他ページに遷移しようとしても、トップページへの強制遷移が発生し、他ページに遷移ができない状態に陥りました。
明日からユーザ側で試験利用と言っているのに、軽くパニックです(笑)

(画面遷移イメージ)

■直接原因

Webシステムの仕様から考えると、ダイレクトアクセス検知によってトップページに強制転送されているのだろうと感じていました。 そして、その直感は当たっており、この仕様に絡んで以下2つの仕組みにより起こった問題であることが分かりました。

① 相対URLでの画面遷移

Webシステムのページ遷移に使用するURLは、Flaskのurl_forというメソッドを使用しており、このメソッドで生成されるURLは相対URLがデフォルトとなっており、このWebシステムでもデフォルト指定にて使用していました。
今回の場合、直前のアクセス元であるALBがhttps → httpに書き換えてリクエストを投げてきているため、受け取ったhttp://~から始まる絶対パスを元に、相対指定でURLが作成されて画面遷移されることになりました。

② HTTPSサイトからHTTPサイト遷移によるリファラ削除

突然何だ?!と思うかもしれませんが、このWebシステムではダイレクトアクセスの検知を、リクエストヘッダ内にあるリファラ(遷移元URL情報)の存在チェックで行っていました。
想定では、ダイレクトアクセスの場合、リファラには遷移元URLが入っていないため、ここをチェックすることでダイレクトアクセスの判定が可能と考えていたためです。
ですが、①の画面遷移を受け付けたブラウザは「HTTPSサイト(安全)」から「HTTPサイト(非安全)」への遷移が発生したと検知し、セキュリティリスク回避のためリファラの内容を削除してリクエストしていました。

その結果、リクエストを受け取ったWebシステムは、リファラなし(=ダイレクトアクセス)と判断し、トップページに強制転送していた訳ですね。

リファラという用語がピンとこない方は、公式ページを参照してみてください。
Referer - HTTP - MDN Web Docs

■根本原因

ここまで、実際に事象を引き起こした仕組みについて説明してきました。
ですが、そもそもAWSのALBを経由しなければ、正常に動作していたわけです。なのに、なぜこの事象が発生したのでしょうか?

それは、AWSのALBを経由したことでリクエスト情報が書き換わったためです。

今回でいうと、ALBからEC2(Web/APサーバ)への転送時に通信プロトコルをhttps → httpに書き換えており、転送リクエストを受け取ったEC2(Web/APサーバ)から見るとリクエスト元はALBなので何も間違ってはいませんし、そういう仕様なのです。
そのため、開発者は、プロキシ経由が発生すると分かった時点で、クライアントのリクエスト情報を正確に取得できるよう設計考慮が必要でした。

■解決方法

それでは、実際に根本原因に対処していきましょう。
今回は、クライアントの情報をちゃんと取得できれば解決するわけです。
でも、リクエスト情報が書き換えられてるのにどうすれば?

ご安心ください!

今回使用したALBの場合、経由前のリクエスト情報が別のヘッダーに退避されています。
それが「X-Forwarded-xxx」ヘッダーです。
「X-Forwarded-xxx」ヘッダーには、いくつか種類があり、通信プロトコルが格納されているヘッダーは「X-Fowarded-Proto」となります。

ヘッダー名 説明
X-Forwarded-For プロキシ経由時、クライアントのIPアドレスが格納されます。複数プロキシ経由時はカンマ区切りで表記され、プロキシ経由ごとに右端にIPアドレスが追加されていきます。
X-Forwarded-Host リクエストヘッダー内でクライアントから要求された元のホストを特定するための事実上の標準となっているヘッダーです。
X-Forwarded-Proto プロキシまたはロードバランサーへ接続するのに使っていたクライアントの通信プロトコル (HTTP または HTTPS) を特定するために事実上の標準となっているヘッダーです。

ただし、これらのヘッダーは標準化されたものでなく、書き換え容易且つ、経由するプロキシ(今回はALB)によって挙動が異なる可能性もあるので、ご注意ください。ALBの仕様については以下に記載がありました。
HTTP ヘッダーと Application Load Balancer

なお、「X-Forwarded-xxx」ヘッダーに関する詳細は以下を参照して確認してみてください。
※「X-Fowarded-xxx」は、現状「X-Forwarded」ヘッダに移行されるようなので「X-Fowarded」ヘッダーに関するリンクも張っておきます。
X-Forwarded-Proto - HTTP - MDN Web Docs
Forwarded - HTTP - MDN Web Docs - Mozilla
RFC 7239 Forwarded HTTP Extension

ここまでの説明から、この事象を解決するには「X-Fowarded-xxx」ヘッダーにあるプロトコル情報を使えば良い。というのが、ふんわり頭に浮かんだのではないかと思います。
それでは、このWebシステムでは、どのように「X-Forwarded-Proto」ヘッダーの値で書き換えればよいのでしょうか?
自分でゴリゴリ実装することも可能ですが、今回使用したPythonのフレームワークであるFlaskでは、既に対応するミドルウェアが提供されていました。
それが「X-Forwarded-For Proxy Fix」というミドルウェアです。

X-Forwarded-For Proxy Fix - Werkzeug

このミドルウェアを使用することで、経由(信頼)するプロキシ数に応じてリクエスト元情報の補正を行うことが可能です。 使用方法などの詳細は、上記公式ページを参照して、確認してみてください。
具体的な実装例は以下となります。

  1. from werkzeug.middleware.proxy_fix import ProxyFix
  2.  
  3. def app():
  4. ~略~
  5. app = Flask(__name__)
  6. app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1)
  7. ~略~

インスタンス作成時に今回問題となった「X-Forwareded-Proto」ヘッダの値を使用して、通信プロトコル情報が補正されるように設定します。 公式を参照すると、「X-Fowareded-Proto」ヘッダーを補正したい場合は、引数にx_proto=(信頼する)プロキシ経由数の形式で指定すれば良いようです。
ヘッダーごとに引数が異なるようなので、詳細は公式参照ください。

上記を設定することで、無事問題は解決することが出来ました。

<おまけ>
対処方法を色々と探していたのですが、他の方法もあるようでした。
プロキシによっては「X-Forwarded-xxx」ヘッダーが設定されていないなどもあると思うので、参考までに紹介しておきます。 ただし、ヘッダーが取得可能な時は、わざわざ手間を増やしたり、セキュリティリスク高める必要はないと思うので、推奨は致しません。

・直接原因① Flaskが認識するリクエスト元プロトコルを"HTTPS"固定に変更する
Flaskのインスタンス生成時、リクエスト元のプロトコル情報を直接"HTTPS"(固定)に書き換えることで対処する方法です。以下、実装例です。
  1. def create_app():
  2. ...省略...
  3. class SchemeFix:
  4. def __init__(self, app):
  5. self.app = app
  6.  
  7. def __call__(self, environ, start_response):
  8. environ['wsgi.url_scheme'] = 'https'
  9. return self.app(environ, start_response)
  10.  
  11. app = Flask(__name__)
  12. app.wsgi_app = SchemeFix(app.wsgi_app)
  13. return app

・直接原因② リファラが削除されないように設定を変更する
直接原因②はリファラが削除されるのが問題なので、削除されないようにすればいいじゃん!という対策です。対象のWebページ(HTML)に対して、以下のようなメタ情報を定義してリファラの送信制限を緩和させます。
  1. <meta name="referrer" content="origin-when-crossorigin">

上記content="origin-when-crossorigin"の記載が該当箇所になります。
この設定の場合、同一のプロトコル水準 (HTTP→HTTP, HTTPS→HTTPS) で同一オリジンのリクエストを行う場合はオリジン、パス、クエリー文字列が送信され、オリジン間リクエストや安全性の低下する移動先 (HTTPS→HTTP) ではオリジンのみを送信します。
今回のダイレクトアクセスチェックでは、リファラの値有無を確認しているため、オリジンのみでも値があれば事象は解消可能となります。
全ての情報が必要な場合はcontent="unsafe-url"を設定することでリファラを取得可能ですが、安全性の面から非推奨となっているため、使用は回避するべきだと思います。

Referrer-Policy - HTTP - MDN Web Docs

■まとめ

Webシステムだけでなく、インフラも含めた全体的な視点でシステム構築をすることの難しさと面白さを知りました。
徐々に仕組みを理解し、謎が解けた時は本当に楽しいですね。
今後も幅広い領域の経験を積みながら、レベルアップしていければと思います。
,

Visual Studioでデバッグ時に「ID XXXXXのプロセスは実行されていません」のエラーからの復帰方法。

オフィス狛 技術部のHammarです。

先日Visual StudioでIIS Expressからデバッグ時に、急に「ID XXXXXのプロセスは実行されていません」と表示されるようになり、全くデバッグが出来なくなりました。
いろいろ先人の復帰方法を見ながら試してみても、どうもうまくいかず軽くハマっていたんですが、意外と簡単な方法で復帰したので、お知らせしたいと思います。

実行環境

  • Windows10
  • Visual Studio 2017
  •       - IIS Express(Google Chrome)よりデバッグ

事象

先日までは普通にデバッグ開始ボタンよりデバッグできていて、ある日同じようにデバッグ開始したところ、起動してブラウザは開くんですが、「このサイトにアクセスできません」のエラーが発生して、上手く接続できていない状態になっていました。
その時、Visual Studioもデバッグモードになっているものの、設定したデバッグポイントが、「ブレークポイントは、現在の設定ではヒットしません。このドキュメントのシンボルが読み込まれていません。」と表示されてしまっていました。
なので、一旦デバッグを停止し、もう一度デバッグを開始したところ、下記のように「ID XXXXXのプロセスは実行されていません」が表示され、以降はデバッグをやり直しても、Visual Studioを起動しなおしても同エラーが出続けてしまう状態に陥ってしまいました。

解消方法

調べていくと、似たような状態になっている記事をいくつか見つけたので、手っ取り早くできる方法を2つ3つやってみたのですが、どうも解決しませんでした。
(「IIS Expressサーバーを起動できません。」というエラーと一緒に発生している記事の内容を主に試してみたので、ちょっと今回の事象とは微妙に違ったのかもしれません)
で、調べをすすめていくと、簡単に解消できた方法が以下の内容です。

■csprojファイルの下記3行を削除する

  1. <DevelopmentServerPort>xxxxx</DevelopmentServerPort>
  2. <DevelopmentServerVPath></DevelopmentServerVPath>
  3. <IISUrl>http://localhost:xxxxx/</IISUrl>

上記はデバッグ時に起動するIIS ExpressのURLや、仮想開発サーバーのポート、パスを設定している部分なのですが、こちら3行を削除して、プロジェクトをリロードすれば、なんとあっさりデバッグが正常にできるようになりました。
ちなみに、上記を削除しても、再度デバッグ実行したタイミングで、記述が自動的に再作成されるので、特に問題は無いようです。

また本エラーの解消方法調べていくと、結構対応方法が多様でバラバラなので、もしかしたら一概に、この方法で解決します!とはならないかもしれないのですが、とりあえず簡単に試せる方法ではあるので、もし同様のエラーに陥った方は、まずは一旦試していただければと思います。

node.jsでnockライブラリを使って外部API呼び出しをモック化させる。

オフィス狛 技術部のHammarです。

node.jsでAPIを実装しているプロジェクトにユニットテストを実施することになり、いろいろと自動化させています。
このユニットテストにおいて、外部のAPIを呼び出す処理は、実際に外部APIを呼ばずに、APIモックを作成してテストするということになり、今回nockというライブラリを使って簡単にモックを作成ができたので、ご紹介したいと思います。

実行環境

※ユニットテストではjestを使います
  • Node.js  v12.14.0
  • jest           v27.5.1
  • nock        v13.2.4


以下代表的なGETとPOSTのテストについてサンプルを書いてみます。

GETの場合

テスト対象となるプログラムの例として、以下GETリクエストのプログラムを作成します。
■testGetRequest.js
const request = require('request');

exports.getUserInfo = async () => {
  // リクエスト作成
  const requests = {
    url: 'https://test.com/users',
    method: 'GET',
    headers: {
      'Content-Type': 'application/json',
    },
    json: true,
  };

  // リクエスト送信
  return new Promise((resolve, reject) => {
    request(requests, async (error, response, body) => {
      if (error) {
        reject(error);
      } else if (response.statusCode !== 200) {
        reject(response);
      } else {
        resolve(body);
      }
    });
  });
};
上記をnockを使ったテストコードを書くと以下のようになります。
■testGetRequest.test.js
const nock = require('nock');

describe('ユーザー情報取得のTest', () => {
  let resData;

  beforeEach(() => {
    // レスポンス
    resData = {
      user_id: '1',
      user_name: 'テスト太郎',
    };
  });

  afterEach(() => {
    nock.cleanAll();
    jest.clearAllMocks();
  });

  it('ユーザー情報取得リクエスト', async () => {
    // APIリクエストのモック
    nock('https://test.com')
      .get('/users')
      .reply(200, resData);

    const repos = await testGetRequest.getUserInfo();

    expect(repos.user_id).toEqual('1');
    expect(repos.user_name).toEqual('テスト太郎');
    expect(nock.isDone()).toBe(true);
  });
上記のnock('https://test.com').get('/users')が、テスト対象プログラム(testGetRequest.js)のgetUserInfo()で呼んでいるAPIリクエストのパスと一致、かつHTTPメソッドも同じにすることによって、resDataに記述したモックするレスポンス内容を返すことができるようになります。
ちなみにnock.isDone()で、リクエストをしたかどうかを判定できます。

続いて、POSTリクエストの例が以下になります。

POSTの場合

■testPostRequest.js
const request = require('request');

exports.entryUserInfo = async (userId, userName) => {
  // リクエストBODY
  const bodyData = {
    user_id: userId,
    user_name: userName,
  };

  // リクエスト作成
  const requests = {
    url: 'https://test.com/users',
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: bodyData,
    json: true,
  };

  // リクエスト送信
  return new Promise((resolve, reject) => {
    request(requests, async (error, response, body) => {
      if (error) {
        reject(error);
      } else if (response.statusCode !== 200) {
        reject(response);
      } else {
        resolve(body);
      }
    });
  });
};
上記のテストコードを書くと以下のようになります。
■testPostRequest.test.js
const nock = require('nock');

describe('ユーザー情報登録のTest', () => {
  let userId;
  let userName;
  let reqBody;
  let resData;
  
  beforeEach(() => {
    userId = '1';
    userName = 'テスト太郎'

    reqBody = {
      user_id: userId,
      user_name: userName,
    };
    // レスポンス
    resData = {
      message: '登録完了しました',
    };
  });

  afterEach(() => {
    nock.cleanAll();
    jest.clearAllMocks();
  });

  it('ユーザー情報登録リクエスト', async () => {
    // APIリクエストのモック
    nock('https://test.com')
      .post('/users', reqBody)
      .reply(200, resData);

    const repos = await testPostRequest.entryUserInfo(userId, userName);

    expect(repos.message).toEqual('登録完了しました');
    expect(nock.isDone()).toBe(true);
  });
});
GETの時と同様に、リクエストのパスとHTTPメソッドを一致させることでモック作成が可能となります。
POSTの場合は、上記.post('/users', reqBody)でリクエストボディの内容を設定できます。
ちなみに、テストを実行した後は、nock.cleanAll()でモックをクリアする必要がありますので、忘れずに。

上記のように、思っていた以上に楽にAPIリクエストのモックを作成することが出来るようになりました。
公式の情報を確認すると、さらに細かくモック設定できそうなので、細かなテストにも対応できそうです!