クラウド・コンピューティングにおけるコンテナとは何か?

コンテナとは?カバー

2013年にDockerが登場して以来、コンテナは爆発的な人気を博している。コンテナによってソフトウェアのデプロイ、配布、管理、スケーリングが容易になるため、すでに多くの企業がワークフローにコンテナを組み込んでいる。

この記事では、クラウド・コンピューティングにおけるコンテナとは何かを説明する。コンテナを使用するメリット、使用例、仮想マシンとの比較、DockerとKubernetesについて説明します。最後に、Back4app ContainersにWebアプリケーションをコーディング、Docker化、デプロイする方法をお教えします!

コンテナの定義

コンテナとは、コード、ランタイム、ライブラリ、環境変数、設定ファイルなど、アプリケーションの実行に必要なすべてを含むスタンドアロンの実行可能パッケージのことだ。コンテナ化されたアプリケーションの素晴らしい点は、ローカルの開発環境からパブリック・クラウドまで、あらゆる場所で実行できることだ。コンテナはサイズが小さく、効率的で、効果的な分離が可能だ。

コンテナには何が含まれていますか?

コンテナを使用するメリット

コンテナを使うことにはいくつかの利点がある。そのいくつかを見てみよう。

効率性

コンテナにはオペレーティング・システム・イメージが含まれないため、従来のサーバーやVMよりもシステム・リソースが少なくて済む。そのため、非常に効率的でサイズも小さく(通常はMB単位)、1台のサーバーで多くのアプリケーションを実行できる。

アプリケーションの分離

コンテナは、アプリケーションとその依存関係をホストシステムから分離する。同時に、OSカーネルやCPU、メモリ、ストレージ、ネットワークなどのシステムリソースを共有することができる。

携帯性

コンテナ化されたソフトウェアは、コンテナ・エンジンがインストールされたマシンであれば、実質的にどのマシンでも同じように実行し、同じように動作する。これにより、異なる環境間でのアプリのデプロイや移動が容易になり、「自分のマシンでは動作する」という問題が解消される。

責任の分離

コンテナは、開発者とIT運用チームの間でタスクと責任を分担することで、責任の分離を可能にする。開発者はアプリケーション・コードと依存関係の作成と保守を担当し、IT運用チームはコンテナと基盤となるインフラのデプロイと管理に集中する。

アプリケーション開発の迅速化

コンテナ化によって、ソフトウェアの開発、テスト、管理、配布が容易になる。コンテナはCI/CDシステムと簡単に統合でき、ソフトウェア開発と出荷プロセスを大幅に加速できる。

簡単なスケーリング

コンテナ化されたアプリケーションは、Kubernetesのようなオーケストレーション・プラットフォームと組み合わせることで、オンデマンドで容易に拡張できる。これにより、コストを最小限に抑えながら、高いワークロードに対応することができます。

コンテナの使用例

コンテナ技術は、IT運用チームだけでなく、開発者にとっても多くのユースケースを持っている。

コンテナ・ネイティブ開発

コンテナネイティブ開発は、コンテナを主要な構成要素として活用するソフトウェア開発アプローチである。コンテナネイティブ開発では、アプリケーションをコンテナとしてパッケージ化し、コンテナ化された環境で実行する。この開発アプローチでは、コンテナが提供するクールな特典をすべて利用できる。

継続的インテグレーションと継続的デリバリー(CI/CD)

CI/CDパイプラインでは、コンテナを使用してアプリケーションをパッケージ化し、自動テストを実行することで、一貫性のある反復可能な方法でアプリケーションをテストおよびデプロイすることができます。コンテナはCI/CDパイプラインの一部として簡単に作成、テスト、デプロイでき、エラーのリスクを低減し、ソフトウェア開発プロセスの全体的な効率を向上させる。

マイクロサービス

コンテナは、マイクロサービス・アーキテクチャに従ったアプリの開発に使用できる。コンテナを使えば、モノリシックなアプリを疎結合できめ細かいサービスの集まりに簡単に分割し、異なるコンテナで実行できる。

開発環境

コンテナは、開発チームが開発環境を素早くセットアップすることを容易にする。ホストOSやホストライブラリに関係なく、一貫した開発環境を提供する。

バッチプロセス

バッチプロセスは簡単にコンテナ化し、クラウドにデプロイできる。各タスクは個別のコンテナ・イメージとしてパッケージ化され、個別のコンテナ・インスタンスとして実行される。これにより、各タスクが独自の環境で実行され、他のタスクに干渉しないため、リソースを効率的に利用できる。

コンテナと仮想マシンの比較

コンテナと仮想マシンは、仮想化に対する2つの異なるアプローチだ。共通点もあるが、まったく異なるものだ。

仮想マシン(VM)は物理ハードウェアを抽象化したものだ。これにより、1台のサーバーを複数のサーバーにすることができる。VMはそれぞれ独自のオペレーティング・システムを持ち、通常はハイパーバイザーによって管理される。VMは、(同じサーバー上で)複数のアプリケーションを実行したり、モノリシックなアプリケーションを実行したり、高度な分離とセキュリティを必要とするアプリケーションを実行したりするのに適している。その欠点は、多くのスペースを占有しがちで、起動にかなり時間がかかることだ。

一方、コンテナはオペレーティング・システム・レベルで仮想化されている。コンテナは同じLinuxカーネルを共有するため占有スペースが小さく、より効率的で起動が速く、拡張性が高く、より多くのアプリケーションを扱うことができる。コンテナはコンテナ・エンジンによって管理される。VMとは対照的に、コンテナの主な用途は、移植性、軽量性、スケーラビリティを必要とするマイクロサービスやアプリケーションである。

コンテナとVMを組み合わせて両方のメリットを得ることも可能だ。

コンテナと仮想マシン(VM)の比較

DockerとKubernetes

コンテナで作業するための最も人気のある2つのツールは、DockerとKubernetesだ。両者がどのように機能するのかを説明し、その違いを見ていこう。

DockerはLinuxベースのオープンソースプロジェクトで、軽量コンテナ内のアプリケーションのデプロイと管理を自動化するために使用される。これにより、コンテナ化されたアプリケーションをさまざまな環境で効率的に動作させることができる。最近では、DockerはLinuxマシンから大規模なクラウド・プロバイダーまで、ほとんどどこにでもある。

最も人気のあるDockerの代替は、PodmanLXDcontainerdだ。

Kubernetes(K8s)は、コンテナ化されたアプリケーションのデプロイ、スケーリング、管理を自動化するためのオープンソースのコンテナ・オーケストレーション・システムだ。2014年のリリース以来、クラウド環境におけるコンテナ化アプリケーションのデプロイと運用のデファクトスタンダードとなっている。Kubernetesの利点には、スケーラビリティ、高可用性、自動運用、インフラの抽象化、ヘルスモニタリングなどがある。

その他のオーケストレーション・プラットフォームには次のようなものがある:AWS ECSNomadRed Hat OpenShiftなどだ。

では、DockerとKubernetesの違いは何だろうか?簡単に言えば、Dockerはコンテナ内でアプリケーションをパッケージ化して配布することを可能にし、Kubernetesは複数のコンテナが互いに調和して動作することを容易にする。

コンテナベースのアーキテクチャを使用したアプリの開発

このチュートリアルでは、シンプルな REST API を作成し、Docker 化し、Back4app Containers にデプロイします。

Back4app Containersとは?

Back4app Containersは、クラウドインフラストラクチャ内のグローバルに分散されたコンテナ上にアプリケーションをデプロイし、スケーリングするための無料のオープンソースプラットフォームです。

DevOpsの心配をすることなく、ソフトウェアに集中し、より速く出荷することができます。このプラットフォームはGitHubと密接に統合されており、CI/CDシステムが組み込まれているため、数分でアプリを立ち上げて実行することができる!

なぜBack4app Containersを使うのですか?

  • GitHubとうまく統合
  • ダウンタイムなしのデプロイメント
  • 簡単な操作と無料ティア
  • 優れたカスタマーサポート

プロジェクト紹介

映画のウォッチリストとして機能するシンプルなREST APIを構築します。ウェブアプリでは、映画の追加や削除などの基本的なCRUD操作ができるようにする。APIを作るにはFlaskフレームワークを使う。最後に、プロジェクトをDocker化し、Back4app Containersへのデプロイがいかに簡単かをデモします。

前提条件

  • Flaskフレームワークの使用経験
  • Dockerとコンテナの基本的な理解
  • GitおよびGitHubの使用能力

コードアプリ

以下のステップでは、Pythonがインストールされている必要があります。Pythonがまだインストールされていない場合は、ダウンロードしてください。

プロジェクトの初期化

まず、アプリ専用のディレクトリを作成し、そこに移動する:

$ mkdir flask-watchlist
$ cd flask-watchlist

次に、新しい仮想環境を作成し、有効化する:

$ python3 -m venv venv && source venv/bin/activate

フレームワークとしてFlaskを使うので、それをインストールしなければならない:

$ (venv) pip install Flask==2.2.2

以下の内容でapp.pyを作成します:

# app.py

from flask import Flask

app = Flask(__name__)
app.config['JSON_SORT_KEYS'] = False


@app.route('/')
def index_view():
    return {
        'detail': 'Hello world!'
    }

このコードはFlaskを初期化し、メッセージを返すシンプルなエンドポイントを作成します。

でサーバーを実行する:

$ flask run

http://localhost:5000/、「Hello world!」というメッセージが表示されるはずだ。

データベース

データベースにはSQLiteを使う。SQLite は組み込み型のサーバレスリレーショナルデータベース管理システムです。データベースでの作業を簡単にするために、Flask-SQLAlchemy— SQLAlchemy のサポートをアプリに追加する Flask の拡張機能 — をインストールします。

インストールするには

$ (venv) pip install Flask-SQLAlchemy==3.0.3

次に、app.pyの先頭に移動し、データベースを初期化するように変更します:

# app.py

db = SQLAlchemy()
app = Flask(__name__)
app.config['JSON_SORT_KEYS'] = False
app.config['SECRET_KEY'] = '5b3cd5b80eb8b217c20fb37074ff4a33'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///default.db"
db.init_app(app)

輸入のこともお忘れなく:

from flask_sqlalchemy import SQLAlchemy

次に、データベース・モデルを定義しよう。

シンプルな映画ウォッチリストアプリを作るので、必要なモデルは1つだけです。Movieモデルを次のように定義します:

# app.py

class Movie(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(128), nullable=False)
    release_date = db.Column(db.Date(), nullable=False)

    is_watched = db.Column(db.Boolean, default=False)
    watched_at = db.Column(db.DateTime, default=None, nullable=True)

    def as_dict(self):
        return {c.name: getattr(self, c.name) for c in self.__table__.columns}

    def __repr__(self):
        return '<Movie %r>' % self.title

データベースを初期化して投入するために、簡単な Python スクリプトを作成します。プロジェクトのルートに移動し、init_db.pyという名前の新しいファイルを以下の内容で作成します:

# init_db.py

from datetime import date

from app import db, app
from app import Movie

with app.app_context():
    db.create_all()

    if Movie.query.count() == 0:
        movies = [
            Movie(title='Fight Club', release_date=date(1999, 9, 15)),
            Movie(title='The Matrix', release_date=date(1999, 3, 31)),
            Movie(title='Donnie Darko', release_date=date(2001, 1, 19)),
            Movie(title='Inception', release_date=date(2010, 7, 16)),
        ]

        for movie in movies:
            db.session.add(movie)

        db.session.commit()

最後にしなければならないのは、スクリプトを実行することだ:

$ (venv) python init_db.py

これにより、データベース、データベース・テーブルが作成され、それらにデータが入力されます。データベースファイルはインスタンスフォルダに置かれます。

APIエンドポイント

我々のウェブ・アプリは以下のエンドポイントを持つ:

  1. / 基本的なAPI情報を返す
  2. /api/は映画のリストを返す。
  3. /api/create/は新しい映画をウォッチリストに追加する。
  4. /api// 特定の映画の詳細を返す
  5. /api/watch// ムービーを見たことにする。

app.pyの一番下にエンドポイントを定義します:

# app.py

@app.route('/')
def index_view():
    return {
        'name': 'flask-watchlist',
        'description': 'a simple app for tracking the movies you want to watch',
        'version': 1.1,
    }


@app.route('/api/')
def list_view():
    json = [movie.as_dict() for movie in Movie.query.all()]
    return jsonify(json)


@app.route('/api/<int:movie_id>/', methods=['GET', 'DELETE'])
def detail_view(movie_id):
    movie = db.get_or_404(Movie, movie_id)

    if request.method == 'DELETE':
        db.session.delete(movie)
        db.session.commit()

        return {
            'detail': 'Movie has been successfully deleted.'
        }
    else:
        return movie.as_dict()


@app.route('/api/create/', methods=['POST'])
def create_view():
    title = request.form.get('title')
    release_date = request.form.get('release_date', type=float)

    if title is None or release_date is None:
        return {
            'detail': 'Please provide the title and release_date.'
        }, 400

    movie = Movie(title=title, release_date=datetime.fromtimestamp(release_date))
    db.session.add(movie)
    db.session.commit()

    return movie.as_dict()


@app.route('/api/watch/<int:movie_id>/')
def watch_view(movie_id):
    movie = db.get_or_404(Movie, movie_id)

    if movie.is_watched:
        return {
            'detail': 'Movie has already been watched.'
        }, 400

    movie.is_watched = True
    movie.watched_at = datetime.now()
    db.session.commit()

    return movie.as_dict()

輸入車のこともお忘れなく:

from datetime import datetime
from flask import request, jsonify

これでアプリはほぼ完成だ。開発サーバーを動かしてみよう:

$ (venv) flask run

映画リストを取得できるかテストする:

$ (venv) curl http://localhost:5000/api/ | jq '.'

[
  {
    "id": 1,
    "title": "Fight Club",
    "release_date": "Wed, 15 Sep 1999 00:00:00 GMT",
    "is_watched": false,
    "watched_at": null
  },
  {
    "id": 2,
    "title": "The Matrix",
    "release_date": "Wed, 31 Mar 1999 00:00:00 GMT",
    "is_watched": false,
    "watched_at": null
  },
  ...
]

Gunicorn

Flaskの開発用サーバーは本番用には適していないので、Gunicornと交換してみよう。Gunicornまたは “Green Unicorn “は、本番環境に対応したUnix用のPython WSGI HTTPサーバーだ。

を実行してインストールする:

$ (venv) pip install gunicorn==20.1.0

パッケージがインストールされたら、次のようにWSGIサーバーを起動します:

$ (venv) gunicorn -w 2 -b 0.0.0.0:5000 app:app

[INFO] Starting gunicorn 20.1.0
[INFO] Listening at: http://0.0.0.0:5000 (1)
[INFO] Using worker: sync
[INFO] Booting worker with pid: 7
[INFO] Booting worker with pid: 8

このコマンドはUNIXベースのオペレーティング・システムでのみ動作することに留意してほしい。

これで2つのGunicornワーカーが起動し、アプリがインターネットに公開されます。アプリにアクセスするには、お気に入りのウェブブラウザを開き、http://localhost:5000。

要件.txt

アプリをDocker化する前にしなければならない最後のことは、requirements.txtファイルを作成することだ。requirements.txtファイルは、プロジェクトの依存関係を指定するために使用する。

それを生成する最も簡単な方法は、実行することである:

$ (venv) pip freeze > requirements.txt

Dockerizeアプリ

以下のステップでは、Dockerをインストールする必要があります。Dockerをインストールする最も簡単な方法は、Docker Desktopをダウンロードすることです。

Dockerがインストールされていることを確認するには、以下を実行する:

$ docker --version

Docker version 20.10.22, build 3a2c30b

Dockerfile

アプリケーションをDocker化するには、Dockerfileを使います。Dockerfileはプレーン・テキスト・ファイルで、ベース・イメージ、環境、環境変数、コマンド、ネットワーク設定、ボリュームなどを定義することができる。

プロジェクト・ルートに以下の内容でDockerfileを作成する:

# syntax=docker/dockerfile:1.4
FROM --platform=$BUILDPLATFORM python:3.10-alpine

# set the working directory
WORKDIR /app

# set environmental variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# install the requirements
COPY requirements.txt /app
RUN --mount=type=cache,target=/root/.cache/pip \
    pip3 install -r requirements.txt

# copy the code to the container
COPY . .

# initialize the database (create DB, tables, populate)
RUN python init_db.py

# expose
EXPOSE 5000/tcp

# entrypoint command
CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0:5000", "app:app"]
  1. ベースイメージにはpython:3.10-alpineを使用。
  2. PYTHONDONTWRITEBYTECODE を 1に設定すると、Python は.pycファイルをディスクに書き込まなくなります。
  3. PYTHONUNBUFFEREDを 1に設定することで、Pythonの出力ストリームがそのままターミナルに送られるようになります。

Dockerfileの書き方の詳細については、Dockerfileリファレンスを参照してください。

.dockerignore

Dockerはイメージをビルドする前に、.dockerignoreファイルを探します。.dockerignoreファイルによって、イメージに含めたくないファイルを定義できる。これにより、イメージのサイズを大幅に縮小することができます。.gitignoreファイルと同じような働きをします。

プロジェクトルートに、以下の内容の.dockerignoreファイルを作成する:

# .dockerignore

.git/
instance/
__pycache__/
.idea/

除外したいディレクトリやファイルがあれば追加してください。

イメージのビルドと実行

続いて、Dockerイメージをビルドしてタグ付けしてみよう。

$ docker build -t flask-watchlist:1.0 .

[+] Building 11.1s (15/15) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                                                                                                                                                                               0.0s 
 => => transferring dockerfile: 32B                                                                                                                                                                                                                                                                                0.0s 
 => [internal] load .dockerignore                                                                                                                                                                                                                                                                                  0.0s 
 => => transferring context: 34B                                                                                                                                                                                                                                                                                   0.0s 
 => resolve image config for docker.io/docker/dockerfile:1.4                                                                                                                                                                                                                                                       0.5s 
 => CACHED docker-image://docker.io/docker/dockerfile:1.4@sha256:9ba7531a0dbc                                                                                                                                                                                  0.0s 
 => [internal] load build definition from Dockerfile                                                                                                                                                                                                                                                               0.0s 
 => [internal] load .dockerignore                                                                                                                                                                                                                                                                                  0.0s 
 => [internal] load metadata for docker.io/library/python:3.10-alpine                                                                                                                                                                                                                                              0.5s 
 => [stage-0 1/6] FROM docker.io/library/python:3.10-alpine@sha256:da5ab5e911253dfb                                                                                                                                                                                0.0s 
 => [internal] load build context                                                                                                                                                                                                                                                                                  0.3s 
 => => transferring context: 182.45kB                                                                                                                                                                                                                                                                              0.2s 
 => CACHED [stage-0 2/6] WORKDIR /app                                                                                                                                                                                                                                                                              0.0s 
 => [stage-0 3/6] COPY requirements.txt /app                                                                                                                                                                                                                                                                       0.0s 
 => [stage-0 4/6] RUN --mount=type=cache,target=/root/.cache/pip     
                      pip3 install -r requirements.txt                                                                                                                                                                                                              7.2s 
 => [stage-0 5/6] COPY . .                                                                                                                                                                                                                                                                                         0.3s 
 => [stage-0 6/6] RUN python init_db.py                                                                                                                                                                                                                                                                            1.5s 
 => exporting to image                                                                                                                                                                                                                                                                                             0.3s 
 => => exporting layers                                                                                                                                                                                                                                                                                            0.3s 
 => => writing image sha256:2671ccb7546a0594807c721a0600a                                                                                                                                                                                                                       0.0s 
 => => naming to docker.io/library/flask-watchlist:1.0 

画像をリストアップすると、新しい画像が表示されるはずだ:

$ docker images

REPOSITORY        TAG       IMAGE ID       CREATED       SIZE
flask-watchlist   1.0       7bce66230eb1   8 hours ago   110MB

最後に、そのイメージを使って新しいDockerコンテナをスピンアップする:

$ docker run -it -p 5000:5000 flask-watchlist:1.0

[2023-02-02 20:08:57 +0000] [1] [INFO] Starting gunicorn 20.1.0
[2023-02-02 20:08:57 +0000] [1] [INFO] Listening at: http://0.0.0.0:5000 (1)
[2023-02-02 20:08:57 +0000] [1] [INFO] Using worker: sync
[2023-02-02 20:08:57 +0000] [7] [INFO] Booting worker with pid: 7
[2023-02-02 20:08:57 +0000] [8] [INFO] Booting worker with pid: 8

dを使用すると、Dockerコンテナをデタッチモードで起動できる。つまり、コンテナはターミナルのバックグラウンドで実行され、入力を受け取ったり出力を表示したりしない。

これで、アプリがコンテナ内で実行されるようになった!http://localhost:5000にアクセスすると、以下のようなレスポンスが返ってくるはずだ:

{
    "name": "flask-watchlist",
    "description": "a simple app for tracking the movies you want to watch",
    "version": 1
}

GitHub

アプリをBack4app Containersにデプロイするには、ソースコードをGitHubのリポジトリにアップロードする必要があります。GitHubに新しいリポジトリを作成し、リモートを追加し、.gitignoreを追加してコードをコミットします。コードをGitHubにアップロードしたら、次のステップに進みます。

アプリをBack4appコンテナにデプロイする

以下のステップでは、Back4appのアカウントが必要です。既にお持ちの場合はログインしてください。そうでない場合は無料アカウントにサインアップしてください。

Back4appを使うには、まずアプリを作成する必要があります。ダッシュボードにログインすると、アプリのリストが表示されます。新しいアプリを作成するには “Build a new app “をクリックしてください。

Back4app アプリ作成

次に、”Containers as a Service “を選択する。

サービスとしてのBack4appコンテナ

まだの場合は、GitHubをBack4appに接続し、デプロイしたいリポジトリをインポートしてください。GitHubが接続されると、リポジトリがテーブルに表示されます。

Select “をクリックして、デプロイしたいリポジトリを選びます。

Back4app Containers Connect リポジトリ

次に、Back4appが環境設定を聞いてくる。アプリ名を決めてください。ここではflask-watchlistとします。他はデフォルトのままで構わない。

最後に「Create App」をクリックすると、アプリが自動的に作成され、デプロイされる。

Back4app Containers 環境を設定する

その後、アプリの詳細にリダイレクトされ、デプロイのログを見ることができます。

Back4appコンテナの成功事例

アプリがデプロイされるまで数分待ちましょう!アプリはBack4app Containers上で公開されました。アプリの動作を確認するには、左側に表示されている緑色のURLをクリックしてください。

結論

この記事を通して、コンテナとは何か、その利点について説明し、あなたのワークフローにコンテナを実装する方法を示しました。ここまでで、あなたは独自のシンプルなREST APIを構築し、それをドッカー化してBack4app Containersにデプロイできるようになったはずです。

GitHubリポジトリから最終的なソースコードを入手する。

今後のステップ

  1. データベースをイメージに保存すべきではありません。現時点では、再デプロイするたびにデータベースがリセットされてしまいます。マネージドPostgreSQLかMySQLインスタンスに切り替えることを検討してください。
  2. Dockerfileを最適化するためのマルチステージビルドについて学びましょう。
  3. ステップバイステップのチュートリアルについては、Dockerコンテナのデプロイの記事をお読みください。

よくあるご質問

コンテナとは何ですか?

コンテナとは、アプリケーションの実行に必要なすべて(コード、ランタイム、ライブラリ、環境変数、設定ファイル)を含むスタンドアロンの実行可能パッケージです。

コンテナを使用する利点は何ですか?

– 効率性
– アプリケーションの分離
– 責任の分離
– アプリケーション開発の高速化

コンテナと仮想マシンの違いは何ですか?

仮想マシン(VM)は物理ハードウェアの抽象化であり、コンテナはオペレーティングシステムレベルで仮想化されます。VMはより高度な分離とセキュリティを提供しますが、コンテナは容量が小さく、効率的でスケーラブルです。

DockerとKubernetesの違いは何ですか?

Dockerはアプリケーションをコンテナ内にパッケージ化して配布するためのツールであり、Kubernetesは複数のコンテナを連携して動作させるのを容易にします。

コンテナベースのアーキテクチャでアプリを開発するには?

1. プログラミング言語を選び、アプリを開発します。
2. DockerfileまたはDocker Composeを使ってアプリをDocker化します。
3. Dockerイメージを作成し、ローカルでテストします。
4. Back4app ContainersのようなCaaSを選び、コードをそこにプッシュします。
サービスのデプロイを待てば完了です!


Leave a reply

Your email address will not be published.