0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Azure DevOps,Terraform,OIDCを利用してキー管理せずにAWSにアプリケーションをデプロイする

Last updated at Posted at 2024-12-12

この記事はAzure DevOps Advent Calendar 2024の13日目の記事です。

はじめに

いつも会社のアドベントカレンダーには参加してますが、たまには別のアドカレにも参加してみようかなと思っていたところ、ちょうどkkamegawaさんの投稿を見かけたので勢いで参加してみました。

ただ、参加してみたのはいいものの私は自分のためにこんな資料を作ってしまうぐらいAzure入門者なので、最近Azure DevOps周りを触ることが増えてきたとはいえ中々ネタが決まりません。

そこで過去に書いたこのあたりの記事を元に、少し拡張した検証をしてみようと思います。

で、実際に検証した構成がこちらです。
image.png

この構成では、アプリはAzure PipelineからECRにコンテナイメージとしてPush、インフラ部分はAWS App Runnerを利用してデプロイします。
App Runnerには自動でECRイメージの変更を検知して新しいバージョンのデプロイをしてくれる機能があるため、ECR以降のデプロイはユーザー側で操作する必要がありません。
また、アプリとインフラどちらをデプロイするかはブランチで制御を行います。(Azure PipelineとTerraform Cloudの機能を利用します)

あとこれが一番嬉しいと思うのですが、このような異なるサービス間で連携させようとした場合、デプロイ用のIAMユーザーを払い出さないといけないケースが出てくることがあり、その場合アクセスキーの管理が必要になって運用的にもセキュリティ的にもあまり推奨されません。

そこで各サービスに統合されているOpen ID Connectの仕組みを利用して、アクセスキーを利用せずにアプリ/インフラのどちらもデプロイします。
インフラのデプロイはAzure Pipelineからもやろうと思ったらできないことはないのですが、Azure Pipelineに統合されたタスクにApp Runnerへのデプロイタスクがないので、AWSCLI等でコマンドを書かないといけません。
Azure PipelineでTerraformを利用する方法でも良いのですが、それだとstateファイルの管理等を考慮する必要があり、Terraform Cloudであればstateファイルの管理を気にせずにデプロイ利用できるので採用しました。

余談

私は業務内容的にクラウド周りの技術検証をよくするのですが、検証環境を毎回手で構築するのは再現性がなく手間なのでTerraformを利用することが多いです。
その際、ローカルのTerraformだと検証ごとにフォルダを分けてるので、数が多くなってくるとどれが構築済みでどれが削除済みなのか忘れてしまうことがあります。
Terraform Cloudを利用するとFree Plan(※)でも500リソースまでは無料で利用できるため、検証時のみリソースを構築して、検証が終わったら削除するようにすることでFree Planの範囲内でもかなり使えますし、Terraform Cloudコンソールからまとめて構築状況を見ることができます。
個人利用でもかなり使いやすいのでオススメです。

Free Organizations

Free organizations are limited to 500 managed resources. Refer to What is a managed resource for more details.
自由組織は500の管理リソースに制限されている。 詳細については、「管理対象リソースとは」を参照してください。

前提

一から全てを説明するのは無理なので、以下は既に作成されている前提とします。

  • Azure DevOps Organizations/Project作成済み
  • Azure Reposリポジトリ作成済み
  • AWSアカウント作成済み
  • Terraform Cloudアカウント/Organizations/Project作成済み

実作業

本記事では基本的にデプロイ以外は画面操作によって構築していきます。
一部過去に類似記事を書いている手順についてはそちらを参照していただき、記事内では詳しく説明しません。
作業としては次の流れで行っていきます。

  1. Azure-AWS間OIDC設定
  2. Amazon ECRリポジトリ作成
  3. Azure Reposリポジトリ作成
  4. デプロイファイル作成
  5. Azure Pipelineパイプライン作成
  6. アプリデプロイ
  7. Terraform Cloud Workspace作成
  8. Terraform Cloud-AWS間OIDC設定
  9. インフラデプロイ
  10. アプリ再デプロイ
  11. インフラ再デプロイ

Azure-AWS間OIDC設定

最初にAzureとAWS間でOIDC設定を行って、AzureからAWSにデプロイするためのIAMロールを準備します。
過去に書いた以下記事で書いてますので詳しく説明まではしませんが、Azure側に若干詰まりどころがあるぐらいで、そこまで複雑な手順にはないと思います。

image.png

Amazon ECRリポジトリ作成

公式のドキュメントの手順に従って、コンテナイメージを格納するECRリポジトリを作成します。
リポジトリ名は「node_app」とします。

image.png

image.png

Azure Reposリポジトリ作成

Azure Reposも同じく公式のドキュメントに従って、Reposリポジトリを作成します。
こちらもリポジトリ名を「node_app」とします。

image.png

デプロイ用ファイル作成

次に作成したReposリポジトリ内にデプロイに利用するファイルを作成します。
以下のような構造になるようにファイルやフォルダを作成します。

.
├─ src
│  └─ default.conf
│  └─ Dockerfile
│  └─ app
│  │  └─ index.html
│  └─ infra
│     └─ provider.tf
│     └─ data.tf
│     └─ output.tf
│     └─ apprunner.tf
└─ README.md

README.md以外のファイルの中身を以下に記載します。

default.conf
server {
        listen 80 default_server;
        listen [::]:80 default_server;

        root /app;

        location / {
        }
}
Dockerfile
FROM alpine:3.6

# nginxのインストール
RUN apk update && \
    apk add --no-cache nginx

# ドキュメントルート
ADD app /app
ADD default.conf /etc/nginx/conf.d/default.conf

# ポート設定
EXPOSE 80

RUN mkdir -p /run/nginx

# フォアグラウンドでnginx実行
CMD nginx -g "daemon off;"
app/index.html
<html>
<body>
TEST
</body>
</html>
infra/provider.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}
infra/data.tf
data "aws_ecr_repository" "node_app" {
  name = "node_app"
}
infra/output.tf
output "service_url" {
  value = aws_apprunner_service.example.service_url
}

output "service_status" {
  value = aws_apprunner_service.example.status
}
infra/apprunner.tf
# IAM Role for App Runner
resource "aws_iam_role" "apprunner_service_role" {
  name = "apprunner-service-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "build.apprunner.amazonaws.com"
        }
      }
    ]
  })
}

# IAM Role Policy Attachment
resource "aws_iam_role_policy_attachment" "apprunner_service_role_policy" {
  role       = aws_iam_role.apprunner_service_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess"
}

# App Runner Service
resource "aws_apprunner_service" "node_app" {
  service_name = "nginx-service"

  source_configuration {
    image_repository {
      image_configuration {
        port = "80"  # Nginxのポート
        runtime_environment_variables = {
          "NGINX_WORKER_PROCESSES" = "auto"
        }
      }
      image_identifier      = "${data.aws_ecr_repository.node_app.repository_url}:latest"
      image_repository_type = "ECR"
    }
    authentication_configuration {
      access_role_arn = aws_iam_role.apprunner_service_role.arn
    }
    auto_deployments_enabled = true
  }

  instance_configuration {
    cpu    = "1024"
    memory = "2048"
  }

  health_check_configuration {
    path = "/"
    protocol = "HTTP"
    healthy_threshold = 1
    interval = 10
    timeout = 5
    unhealthy_threshold = 5
  }

  tags = {
    Environment = "dev"
    Name        = "nginx-service"
  }
}

Azure Pipelineパイプライン作成

Azure DevOpsコンソールからAzure Pipeline画面を開き、「New pipeline」から新しいパイプラインを作成します。
image.png

Azure Repos Gitを選択します。
image.png

先ほど作成した「node_app」を選択します。
image.png

後で上書きするので正直どれでも良いですが「Docker」を選択します。
image.png

Dockerfileが格納されている場所が表示されているので、Reposリポジトリ内のDockerfileパスと一致しているか確認します。
「$(Build.SourcesDirectory)」はBuild時のカレントディレクトリで、Reposリポジトリのルートディレクトリと思ってもらってOKです。
問題なければ「Validate and configure」を押下します。
image.png

Azure Pipelineが参照するpipeline YAMLが自動で作成されます。
先ほどDockerを選択したので、Dcokerfileの内容を元にBuild Imageするタスクがデフォルトで作成されています。
image.png

上記pipeline YAMLの内容は全選択→削除し、以下の内容を貼り付けてパイプラインを作成します。

azure-pipeline.yml
# Docker
# Build a Docker image
# https://docs.microsoft.com/azure/devops/pipelines/languages/docker

trigger:
- release

pool:
  vmImage: ubuntu-latest

resources:
- repo: self

variables:
  tag: '$(Build.BuildId)'

stages:
- stage: Build
  displayName: Build image
  jobs:
  - job: Build
    displayName: Build
    steps:
    - task: Docker@2
      displayName: Build an image
      inputs:
        command: build
        dockerfile: '$(Build.SourcesDirectory)/src/Dockerfile'
        buildContext: '$(Build.SourcesDirectory)/src'
        repository: $(DOCKER_REPOSITORY_NAME)

    - task: ECRPushImage@1
      inputs:
        awsCredentials: 'aws-oidc-user2'
        regionName: 'ap-northeast-1'
        imageSource: 'imagename'
        sourceImageName: $(DOCKER_REPOSITORY_NAME)
        sourceImageTag: $(Build.BuildId)
        pushTag: latest
        repositoryName: $(DOCKER_REPOSITORY_NAME)

2つだけ特筆事項があります。
まず1つ目が以下の変数設定です。

$(DOCKER_REPOSITORY_NAME)

この変数は作成したパイプラインに紐付く変数で、パイプラインの編集画面の「Variables」から設定できます。
今回は次のように設定します。設定内容は最初に作成したECRリポジトリの名前にします。(どちらも"node_app"にしたのでわかりづらいとは思いますが。。)

  • Name:DOCKER_REPOSITORY_NAME
  • Value:node_app

image.png

image.png

image.png

2つ目が「task: ECRPushImage@1」タスクの以下の部分です。

awsCredentials: 'aws-oidc-user2'

ここではAzure-AWS間OIDC設定で作成したAzure DevOpsのService connections内のコネクション名を指定する必要があります。

Service connectionsはProject Settingsから確認ができます。

image.png

うまく設定でできていれば次のような表示になっているはずです。
Role to AssumeはAzureからAWSにデプロイするのに利用するIAMロールARNなので、ご自身の環境で構築する際はよしなに読み替えてください。

image.png

ここまででパイプラインの作成は完了です。

アプリデプロイ

まだインフラを構築していないのでアプリケーションは実行されませんが、一旦コンテナイメージが正常にPushされるか確認してみます。
Azure Pipelines画面から作成した「node_app」パイプラインを開いて、「Run pipeline」を実行します。

image.png

設定や実装が上手くいっていればECRPushImageタスクが完了するはずです。

image.png

AWSマネジメントコンソールから確認するとコンテナイメージが1つ増えていることがわかります。

image.png

Azure Pipelineパイプラインを作成する際に、以下の設定を入れてパイプラインの実行トリガーをreleaseブランチに変更が入った場合に実行されるように変更したので、Azure Reposからreleaseブランチを作成しておきます

trigger:
- release

image.png

image.png

image.png

インフラ構築時に利用するため、release_infraブランチも同じ要領で作っておきます。

image.png

Terraform Cloud Workspace作成

アプリ側の実装・動確が終わったので、次はインフラデプロイのための環境を構築していきます。
Terraform Cloudにログインして以下の流れでWorkspaceを作成します。

image.png

image.png

今回はAzure DevOpsと連携するので「Version Control Workflow」を選択します。

image.png

接続するProviderを聞かれるので、「Azure DevOps Service」を選択します。

image.png

以下のドキュメントを参考にしつつTerraform CloudとAzure DevOps Servicesの接続設定をします。

必要なのは次の5ステップになります。

  1. Azure DevOps Services Profileから、新しいアプリケーションを作成する
  2. HCP Terraformで、プロバイダを設定する
  3. HCP Terraformで、高度な設定を構成する (オプション)
  4. 対象リポジトリを指定する
  5. Workspaceの詳細設定をする

事前設定

Terraform Cloudとの接続設定をする前に、Azure DevOps側で次の作業を行う必要があります。

Organization Settings>Policies>Application connection policiesから「Third-party application access via OAuth」を有効化します。
image.png

Azure DevOps Services Profileから、新しいアプリケーションを作成する

まずはAzure DevOps側でTerraform Cloudのアプリケーションを登録します。
作成画面にURLが2つあるので、どちらかをコピーしてAzure DevOpsにログイン済みのブラウザの別タブで開きます。
※私の環境では上のほうのみアクセスできました

image.png

アクセスすると次のような画面が表示されます。
image.png

先ほどのTerraform Cloud側の画面に必要な情報が載っているため、この情報を転記していきます。
image.png

一旦必須のところだけ入力してページ下部にある「Create Application」を押下します。
image.png

すると次のようなページに遷移するので、次の手順でApp IDとClient Secretを利用します。
image.png

HCP Terraformで、プロバイダを設定する

Terraform Cloudコンソールに戻って、先ほど確認したApp IDとClient Secretを転記します。
Nameはオプションなので必要であればわかりやすい名前を付けます。

image.png

たまに半角スペースが入り込んでエラーが出たりするのでご注意ください。

image.png

「Connect and continue」を押下するとTerraform CloudがAzure DevOpsに対してアクセスリクエストを送るので、Azure DevOps側でそのリクエストを承認します。
Terraform CloudとAzure DevOpsの設定を別ブラウザで行っていた場合は、次のようにMSアカウントのサインイン画面が表示されることがあります。

image.png

その場合はURLを丸っとコピーして対象のブラウザに貼り付けすると以下の画面が表示されると思います。

image.png

上記画面でAceeptを押下してリクエストを許可します。

HCP Terraformで、高度な設定を構成する (オプション)

オプションとして、Azure DevOpsからアクセスできる対象のWorkspaceの指定やPolicyやModules等の有効化やSSHキーペアの設定ができます。
今回はプロジェクトの指定だけをして「Skip and finish」を押下します。
image.png

対象リポジトリを指定する

Workspaceを連携するリポジトリを選択します。
今回は前の手順で作成した「node_app」リポジトリを選択します。
image.png

Workspaceの詳細設定をする

最後にWorkspaceの詳細設定を行います。

Workspace Nameにはわかりやすく「nodeapp_ws」という名前を入力します。

image.png

この画面ではWorkspaceを実行するトリガーやオートApply等の設定が可能です。

以下ではTerraformを実行するWorking Directoryを指定することができるので、Azure Reposリポジトリの構成からTerraformコードが格納されているパス「src/infra」を記載します。
Auto-applyを設定すると自動でデプロイされてしまうため、今回は手動でApply操作をするためにチェックはなしにします。

image.png

次はTerraformをどのようにトリガーするかを設定します。
ここではBranchベーストリガーを選択し、アプリとブランチを分けるために「release_infra」を記載します。
また、トリガーを特定のファイルが更新されたタイミングで実行されるように設定も可能ですが、ここでは指定したリポジトリ/フォルダ配下が更新されたらトリガーされるようにします。

image.png

最後はサブモジュールをクローリングするかの設定なので、チェックなしのまま「Create」を押下してWorkspaceを作成します。

image.png

以下が表示されたらWorkspaceの作成は完了です。

image.png

Terraform Cloud-AWS間OIDC設定

WorkspaceができたらTerraform CloudとAWS間のOIDC設定を行って、AWSへのデプロイができるようにします。
AWSとのOIDC設定手順は以下ドキュメントに記載があります。

次の3ステップで設定していきます。

  1. OIDCアイデンティティプロバイダの作成
  2. IAMロールの作成
  3. Terraform Cloud Workspaceに変数を設定

OIDCアイデンティティプロバイダの作成

基本的にはドキュメントの以下の箇所を確認すればOIDC ID Providerを作成できると思います。

次の設定を行います。設定値はこちらに記載されています。

image.png

作成できると以下のようになります。

image.png

IAMロールの作成

次に作成したID Providerに紐付くIAMロールを作成します。
デプロイ時に利用されるIAMロールになりますので、権限を制御したい場合はこのロールのポリシーを制限する必要があります。

ID Providerページの「Assign role」を押下して、「Create a new role」を選択します。

image.png

IAMロールの信頼ポリシーを設定します。
次の内容で設定します。

  • Identity provider:app.terraform.io
  • Audience:aws.workload.identity
  • Organization:[Terraform Cloudの該当Organization名]
  • Project:[Terraform Cloudの該当のProjecto名] ※今回は * で記載
  • Workspace:[新規作成したTerraform Cloud Workspace名]
  • Run Phase:[Plan or Apply] ※今回は * で記載

image.png

今回は検証なのでAdministratorAccessを付与します。

image.png

IAMロールの名前を入力して「create role」を押下します。

image.png

作成できたら、IAMロールのARNの値をメモしておきます。(後で使います)

image.png

Terraform Cloud Workspaceに変数を設定

作成したWorkspaceのVariablesページを開いて「Workspace variables」を以下のように設定します。

image.png

変数設定時に「Terraform variable」と「Environment variable」の2つのカテゴリがありますが、「Environment variable」を利用します。

image.png

ここまでできたらTerraform CloudからAWSにデプロイする設定は完了です。

インフラデプロイ

試しに手動で動かすため、Azure Reposのrelease_infraブランチを開いて、src/infra/provider.tfを以下のように修正してCommitします。

image.png

src/infra/provider.tf
terraform {
  required_version = ">= 0.14"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

うまく動かなかったので、WorkspaceのVersion Controlの「Automatic speculative plans」のチェックを外して保存します。

image.png

output.tfを空にしてCommitしてみます。

image.png

WorkspaceのRunsページを見ると処理が開始され、Planまで実行されていることがわかります。

image.png

「Confirm & apply」を押下します。

image.png

Applyが始まってデプロイが開始しました。
App Runnerは若干構築に時間がかかります。

image.png

Applyが成功しました。

image.png

AWSマネジメントコンソールからApp Runnerコンソールを見るとリソースが作られていることが確認できました。

image.png

App RunnerはアプリケーションにアクセスできるURLを「Default domain」で払い出されるので、ブラウザからアクセスしてみるとちゃんとindex.htmlに実装したTESTという文字が表示されていることがわかります。

image.png

インフラのデプロイも成功しましたので、これでアプリとインフラのデプロイパイプラインの構築が完了しました。

アプリ再デプロイ

デプロイパイプラインが作れたので、コードを変更して自動で反映されるか確認してみます。
また、アプリとインフラの更新がそれぞれのパイプラインに影響しないことも確認します。

まずはアプリ側のファイルの更新を行います。
releaseブランチに移動後、index.htmlの"TEST"を"TEST Update"に修正してCommitします。
※本来であればmainブランチからPR・マージするべきですが今回は手順を省略します。

image.png

対象のAzure Pipelineのパイプラインが実行されていることが確認できます。

image.png

ECRリポジトリを見るとイメージが更新されていました。

image.png

App Runnerは対象のECRリポジトリをウォッチしているので、更新があれば自動でデプロイが走っていることがわかります。

image.png

修正した内容がちゃんと反映されました。

image.png

Terraform Cloudのほうは実行されていないことも確認できます。

image.png

無事アプリについてはデプロイパイプラインが動きました。

インフラ再デプロイ

今度はインフラのデプロイパイプラインを動かします。
構築したApp RunnerにはWAFが紐付けられていないので、WAFを作成してApp Runnerと紐付けるようにします。
また、WAFで自身のIPアドレス以外からのアクセスを拒否します。
release_infraブランチに移動して以下の修正を入れてCommitします。

image.png

apprunner.tf
# IAM Role for App Runner
resource "aws_iam_role" "apprunner_service_role" {
  name = "apprunner-service-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "build.apprunner.amazonaws.com"
        }
      }
    ]
  })
}

# IAM Role Policy Attachment
resource "aws_iam_role_policy_attachment" "apprunner_service_role_policy" {
  role       = aws_iam_role.apprunner_service_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess"
}

# App Runner Service
resource "aws_apprunner_service" "node_app" {
  service_name = "nginx-service"

  source_configuration {
    image_repository {
      image_configuration {
        port = "80"  # Nginxのポート
        runtime_environment_variables = {
          "NGINX_WORKER_PROCESSES" = "auto"
        }
      }
      image_identifier      = "${data.aws_ecr_repository.node_app.repository_url}:latest"
      image_repository_type = "ECR"
    }
    authentication_configuration {
      access_role_arn = aws_iam_role.apprunner_service_role.arn
    }
    auto_deployments_enabled = true
  }

  instance_configuration {
    cpu    = "1024"
    memory = "2048"
  }

  health_check_configuration {
    path = "/"
    protocol = "HTTP"
    healthy_threshold = 1
    interval = 10
    timeout = 5
    unhealthy_threshold = 5
  }

  tags = {
    Environment = "dev"
    Name        = "nginx-service"
  }
}

# WAF Web ACL
resource "aws_wafv2_web_acl" "apprunner_waf" {
  name        = "apprunner-waf"
  description = "WAF for App Runner service with IP whitelist"
  scope       = "REGIONAL"

  default_action {
    block {}  # デフォルトですべてのトラフィックをブロック
  }

  # IP ホワイトリストルール
  rule {
    name     = "AllowSpecificIP"
    priority = 0  # 最優先で評価

    action {
      allow {}
    }

    statement {
      ip_set_reference_statement {
        ip_set_forwarded_ip_config {
          fallback_behavior = "MATCH"
          header_name       = "X-Forwarded-For"
          position         = "FIRST"
        }
        arn = aws_wafv2_ip_set.whitelist.arn
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name               = "AllowSpecificIPMetric"
      sampled_requests_enabled  = true
    }
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name               = "AppRunnerWafMetric"
    sampled_requests_enabled  = true
  }
}

# IP セットの定義
resource "aws_wafv2_ip_set" "whitelist" {
  name               = "apprunner-whitelist"
  description        = "Whitelisted IP addresses"
  scope              = "REGIONAL"
  ip_address_version = "IPV4"
  addresses          = ["[自身のIPアドレス]/32"]
}

# WAF Web ACL Association
resource "aws_wafv2_web_acl_association" "apprunner_waf_association" {
  resource_arn = aws_apprunner_service.node_app.arn
  web_acl_arn  = aws_wafv2_web_acl.apprunner_waf.arn
}

Planは成功したので次はApplyします。

image.png

1リソースのみエラーが出ました。
WAFのログの設定で失敗しているようです。

image.png

今回は必須ではないので、apprunner.tfファイル内の以下の箇所を削除して再度Commitします。

# Logging Configuration for WAF
resource "aws_cloudwatch_log_group" "waf_log_group" {
  name              = "/aws/waf/apprunner"
  retention_in_days = 30
}

resource "aws_wafv2_web_acl_logging_configuration" "waf_logging" {
  log_destination_configs = [aws_cloudwatch_log_group.waf_log_group.arn]
  resource_arn           = aws_wafv2_web_acl.apprunner_waf.arn
}

Applyが完了しました。

image.png

App Runnerリソースを確認すると先ほど作成したWAFが設定されていることがわかります。

image.png

試しにIPアドレスを変えてアクセスしてみると想定通りアクセスが拒否されました。

image.png

ECRリポジトリのコンテナイメージも増えていないので、アプリ側のパイプラインも動いてなさそうです。

image.png

これでインフラについても正常にデプロイパイプラインが動きました。

おわりに

担当日という締め切りに追われて思いつきでネタを決めましたが、思ったよりもボリュームが多くて若干後悔しました。
Azure DevOps、Terraform、OIDC、AWSという闇鍋みたいな組み合わせを使う人がどれだけいるんだろうかと思いつつ、部分部分の設定手順は参考にはなるんじゃないかとは思ってます。

この記事がどなたかの参考になれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?