以下からの続きとなります。
GCPでのWebサービスの作り方(Python、Flask、MYSQL、Mail、Apache) 1/2
■ ステップ4)GCPにWebアプリケーション環境をセットアップ
①Ubuntuにログイン
GCE(Ubuntu)のVMインスタンスを選択し、SSHでログインをします。(VMインスタンスを開始しておくこと)SSHプルダウンをから「ブラウザウィンドウで開く」を選択します。
ブラウザ上でターミナルが開きます。
※rootでがログインが必要な場合には、以下で、パスワードを設定してください。
sudo passwd root
②各種パッケージのインストール
・Anacondaのインストール
Python、Flaskの各種パッケージを個別にインストールすることもできますが、ここは一式入っているAnacondaをまずはインストールします。
今回は、以下から Anaconda 3のLinux用インストーラを、いったん自分のPCにダウンロードしてインストールしました。
https://www.anaconda.com/products/individual#Downloads
ダウンロードした「Anaconda3-2021.05-Linux-x86_64.sh」をSSHの設定→アップロード機能を使ってUbuntuに送ります。(ログインユーザのカレントに保存されます)
あとは、ターミナルからインストーラーを実行し、インストールします。
・MySQL接続モジュールのインストール
PythonからMySQLと接続するためのモジュールをインストールします。
以下をターミナルから実行します。
sudo ./pip3 install mysql-connector-python
※ 類似のものに「mysql-connector」がありますが、 varcharがすべてbytearrayで返却されるので使えません。
・Apacheのインストール
Apacheをインストールします。
以下をターミナルから実行します。
パッケージの更新
sudo apt-get update
Apache2のインストール
sudo apt-get install apache2
sudo apt install apache2-dev
・ Apache連結用モジュールのインストール
FlaskとApacheを連結するために必要なWSGIモジュールをインストールします。
以下をターミナルから実行します。
sudo ./pip3 install mod_wsgi
インストール後、以下を実行します。
~/.local/bin/mod_wsgi-express install-module
そうすると以下のような文字列が返却されます。あとで使うので、記録しておきます。
LoadModule wsgi_module “/usr/lib/apache2/modules/mod_wsgi-py38.cpython-38-x86_64-linux-gnu.so”
WSGIPythonHome “/home/ホームディレクトリ/anaconda3”
③MySQLにユーザとデータベースを作成
Webアプリケーションからアクセスするためのユーザを登録しておきます。
GCPのダッシュボートからSQL画面を開き、インスタンスを選択し、ユーザの追加を実施します。
たとえば、user001を新規に追加し、パスワードを設定します。
次に、Webアプリケーションで使うデータベースを作成します。
そのために、SSHのターミナルからmysqlのクライアントツールを使います。
そして、 mysqlのクライアントツール を使うためだけに、 SSHのターミナルから 以下を実行し、GCEのUbuntuにMYSQL80をインストールします。
sudo apt-get install mysql-community-server
MySQLのクライアントツールで、MySQLのインスタンスに接続するには、IPアドレスを知っておく必要があります。
GCPのダッシュボートからSQL画面を開き、インスタンスを選択すると、プライベートIPアドレスがそれに当たります。
SQL言語で書かれたスクリプトをテキストファイルで作成し、GCEのUbuntuにアップロードします。
例えば、以下のようなデータベースの作成、テーブルの作成のスクリプトです。
[SQL.TXT] # データベース作成 CREATE DATABASE TestDB; # QAテーブル作成 create table TestDB.QA_Tbl( id int unsigned not null primary key auto_increment, #QA ID dt datetime, #日時 name varchar(256), #お名前 email varchar(256), #メールアドレス comment varchar(2048) #内容 );
SSHのターミナルから以下を実行後、パスワードを入力し、 MySQLのクライアントツールを起動します。
mysql -h 10.74.128.3 -u user001 -p
MySQLのクライアントツールのプロンプトから上記のスクリプトを実行します。
mysql> source <スクリプトファイルの絶対パス>
その結果、データベースとテーブルがMySQLのインスタンス内に作成されます。
確認のため、 MySQLのクライアントツール のプロンプトから以下のSQL文を入力します。
(作成されたデータベース名が表示されます)
mysql> show databases;
④ Python+Flask のWebアプリケーションのセットアップ
Webアプリケーションは、自分のPCで開発した後、GCEのUbuntu上にアップロードします。
今回は、ブラウザから質疑(QA)を受け取り、MySQLへのデータベースに登録し、回答者へメール送信するテストプログラムを作成します。
メインとなるソースコード/HTMLは、以下です。
# -*- coding: utf-8 -*-
import re
import time
import requests
import datetime
from flask import render_template
from flask import request
from flask import Flask
from flask import abort
from flask import make_response
from flask import redirect
from flask import url_for
from gevent.pywsgi import WSGIServer
# DBクラス
from DbClass import DbClass
# MAILクラス
from MailClass import MailClass
#Flaskアプリ
app = Flask(__name__)
# 問合せメール送信先
CONST_QA_EMAIL_TO='manager@reasvr.com'
# QAをDBに保存する関数
def SaveDb(dt,name,email,comment):
id = 0
db = DbClass()
if db.Connect() == True :
id=db.InsertQA_Tbl(dt,name,email,comment)
db.Disconnect()
return id
# QAメールの送信関数
def SendUserQAMail(id,dt,name,email,comment):
result = False
try:
Mail = MailClass()
if Mail.Connect('',0,'','') == True :
msg=Mail.CreateMail(CONST_QA_EMAIL_TO,id,dt,name,email,comment)
Mail.SendMail(msg)
Mail.Disconnect()
result = True
except:
result = False
return result
@app.route('/', methods=['GET', 'POST'])
def index():
return redirect(url_for('user_QA'))
# 問い合わせ画面
@app.route('/user_QA', methods=['GET', 'POST'])
def user_QA():
html = 'user_QA.html'
return render_template(html)
@app.route('/send_user_QA', methods=['GET', 'POST'])
def send_user_QA():
if request.method == 'POST':
name = request.form['name']
email = request.form['email']
comment = request.form['comment']
else:
name = request.args.get('name')
email = request.args.get('email')
comment = request.args.get('comment')
if name == None :
name = ''
if email == None :
email = ''
if comment == None :
comment = ''
if name == '' or email == '' or comment == '':
message = 'お名前、メールアドレス、お問い合わせ内容を入力してください'
html = 'user_QA.html'
return render_template(html,message=message)
# QAをDBに保存する
dt = datetime.datetime.now()
id=SaveDb(dt,name,email,comment)
if id > 0:
# QAメールを送信する
result=SendUserQAMail(id,dt,name,email,comment)
if result == True:
message = 'お問合せありがとうございました'
html = 'result_user_QA.html'
return make_response(render_template(html,message=message))
message = 'もう一度、お問合せ下さい'
html = 'user_QA.html'
return make_response(render_template(html,message=message))
if __name__ == '__main__':
app.debug = True
host='127.0.0.1'
port = 5000
host_port = (host, port)
server = WSGIServer(
host_port,
app
)
server.serve_forever()
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- inital scale -->
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="/static/common.css" />
<script>
// -------------------------------------------------------------------
// メールアドレスチェック関数
// -------------------------------------------------------------------
function CheckMailAddress( mail ) {
var mail_regex1 = new RegExp( '(?:[-!#-\'*+/-9=?A-Z^-~]+\.?(?:\.[-!#-\'*+/-9=?A-Z^-~]+)*|"(?:[!#-\[\]-~]|\\\\[\x09 -~])*")@[-!#-\'*+/-9=?A-Z^-~]+(?:\.[-!#-\'*+/-9=?A-Z^-~]+)*' );
var mail_regex2 = new RegExp( '^[^\@]+\@[^\@]+$' );
if( mail.match( mail_regex1 ) && mail.match( mail_regex2 ) ) {
// 全角チェック
if( mail.match( /[^a-zA-Z0-9\!\"\#\$\%\&\'\(\)\=\~\|\-\^\\\@\[\;\:\]\,\.\/\\\<\>\?\_\`\{\+\*\} ]/ ) ) { return false; }
// 末尾TLDチェック(〜.co,jpなどの末尾ミスチェック用)
if( !mail.match( /\.[a-z]+$/ ) ) { return false; }
return true;
} else {
return false;
}
}
// 入力チェック
function CheckInput()
{
var error = "";
if(document.form_cond.name.value == "")
{
error = "お名前が入力されていません";
}
else if(document.form_cond.email.value == "")
{
error = "メールアドレスが入力されていません";
}
else if(document.form_cond.comment.value == "")
{
error = "お問合せ内容が入力されていません";
}
// 入力値のチェック
if(error == "")
{
if(CheckMailAddress( document.form_cond.email.value ) == false)
{
error = "正しいメールアドレスを入力してください";
document.form_cond.email.value = "";
}
}
if(error != "")
{
alert(error);
}
else
{
// 送信
document.form_cond.submit();
}
}
</script>
</head>
<body>
<div id="condition" class="centering_parent" >
<label class="error_label">【お問合せ】</label>
<br>
<label class="error_label">{{ message }}</label>
<br>
<form action="/send_user_QA" method="post" name="form_cond" class="form-inline">
<ul>
<li class="title">
<label for="title">以下を入力してください </label>
<br>
<br>
</li>
<li class="name">
<input type="text" class="user_textbox" id="user" name="name" maxlength="24" size="24" placeholder="お名前(必須)">
</li>
<br>
<li class="email">
<input type="text" class="user_textbox" id="user" name="email" maxlength="24" size="24" placeholder="メールアドレス(必須)">
</li>
<br>
<li class="comment">
<label for="comment_label" class="comment_label">【お問合せ内容】(必須)</label>
<br>
<textarea class="comment_textbox" id="comment_textbox" name="comment" rows="10" cols="60"></textarea>
</li>
<br>
<li class="submit">
<br>
<a href="#" class="btn-flat-border" onclick="CheckInput();">問合わせる</a>
</li>
</ul>
</form>
<br>
<br>
<ul class="comment" class="centering_parent" >
<a href="/"><label for="regist">戻る</label></a>
</ul>
</div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- inital scale -->
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="/static/common.css" />
</head>
<body>
<div id="condition" class="centering_parent" >
<br>
<label>{{ message }}</label>
<br>
<br>
<a href="/"><label>戻る</label></a>
</div>
</body>
</html>
@charset "UTF-8";
body{
-webkit-text-size-adjust: 100%;
}
.centering_parent {
text-align: center; /* 中央寄せ */
}
.user_textbox{
position: relative;
display: block;
width: 150px;
margin-top: 0px;
padding: 10px;
border: solid;
border-radius: 5px;
font-size: 12px;
color: #a0a0a0;
outline: none;
margin-left: auto;
margin-right: auto;
text-align: left;
}
.password_textbox{
position: relative;
display: block;
width: 150px;
margin-top: 0px;
padding: 10px;
border: solid;
border-radius: 5px;
font-size: 12px;
color: #a0a0a0;
outline: none;
margin-left: auto;
margin-right: auto;
text-align: left;
}
.btn-flat-border {
display: inline-block;
padding: 5px;
text-decoration: none;
color: #67c5ff;
border: solid 2px #67c5ff;
border-radius: 3px;
transition: .4s;
width: 120px;
font-size: 14px;
}
.btn-flat-border:hover {
background: #67c5ff;
color: white;
}
.submit{
width: 200px;
font-size: 18px;
margin-left: auto;
margin-right: auto;
text-align: center;
}
.comment{
width: 250px;
font-size: 18px;
margin-left: auto;
margin-right: auto;
text-align: center;
}
ul li {
list-style: none;
display: inline;
text-align: center;
}
ul li {
list-style: none;
display: inline;
text-align: center;
}
label {
float: none;
font-size: 14px;
color: #a0a0a0;
width: 200px;
display: inline;
}
.error_label {
float: none;
font-size: 14px;
color: #f85104;
width: 200px;
display: inline;
}
ul {
width: 300px;
margin: 0 auto;
display: inline;
}
input{
display: inline;
}
form{
display: inline;
}
div {text-align:center;}
table{
width: 100%;
border-spacing: 0;
width:300px;
text-align: center;
}
table th{
border-bottom: solid 2px #fb5144;
padding: 10px 0;
width: 150px;
text-align: center;
font-size: 18px;
color: #a0a0a0;
}
table td{
border-bottom: solid 2px #ddd;
text-align: center;
padding: 10px 0;
width: 150px;
font-size: 22px;
color: #a0a0a0;
}
body {
position: static;
overflow: auto;
height: 100vh; /* これを追加 */
}
MySQLと接続するDBクラス(DbClass)は以下のようなものです。
MySQLインスタンスのローカルIPアドレス、ポート(3306)、データベース名、ユーザ名、パスワード、文字コードなど、一式を指定して、接続し、テーブル(QA_Tbl)にレコードを追加します。
# -*- coding: utf-8 -*-
import mysql.connector
from mysql.connector import errorcode
# DB関連定数 ★GCP
CONST_DB_HOST = '10.74.128.3' # MySQLインスタンスのローカルIPアドレス
CONST_DB_PORT = 3306 # MySQLインスタンスのポート
CONST_DB_NAME = 'TestDB' # データベース名
CONST_DB_USER = 'user001' # ユーザ名
CONST_DB_PASSWORD = 'xxxxxxxx' # ユーザのパスワード
CONST_DB_CHARSET = 'utf8' # 文字コード
# DBクラス
class DbClass:
_cnn = ""
_cur = ""
def Connect(self):
try:
self._cnn = mysql.connector.connect(host=CONST_DB_HOST,
port=CONST_DB_PORT,
db=CONST_DB_NAME,
user=CONST_DB_USER,
password=CONST_DB_PASSWORD,
charset=CONST_DB_CHARSET,
auth_plugin='mysql_native_password'
)
self._cnn.autocommit = True
self._cur = self._cnn.cursor(prepared=True)
return True
except (mysql.connector.Error) as e:
return False
def InsertQA_Tbl(self,dt,name,email,comment):
try:
# レコードの登録
sql = 'insert into QA_Tbl SET dt = ?,name = ?,email = ? ,comment =?;'
self._cur.execute(sql,(dt,name,email,comment))
id = self._cur.lastrowid
return id
except (mysql.connector.Error) as e:
return 0
def Disconnect(self):
# データベースから切断
if self._cur != "":
self._cur.close()
if self._cnn != "":
self._cnn.close()
def __init__(self):
self._cnn = ""
self._cur = ""
def __del__(self):
self._cnn = ""
self._cur = ""
if __name__ == '__main__':
row_dic = {}
print(len(row_dic))
# test
db = DbClass()
db.Connect()
row_dic = {}
row_dic['dt'] = 0
row_dic['name'] = 'test'
row_dic['email'] = 'test@test'
row_dic['comment'] = 'test'
id = db.InsertQA_Tbl(row_dic)
db.DisConnect()
SendGridと接続するMailクラス(MailClass)は以下のようなものです。
メール送信のSmtpLibを使って、SendGridと接続し、メール送信します。
その際、Postfixの動作するローカルアドレスとポート、SendGridのアカウントとパスワード、申請した送信元ドメインを指定します。
直接、インターネット上のメールサーバと接続するときと違って、SMTP-AUTHによる認証は不要です。
# -*- coding: utf-8 -*-
import smtplib
import email
from email.message import EmailMessage
from email.mime.text import MIMEText
import datetime
#SendGrid ★GCP
CONST_SERVER = 'localhost' # Postfixが動作するUbuntuのローカルアドレス
CONST_PORT = 25 # Postfixのポート
CONST_ACCOUNT = 'XXXXXXXX@kke.com' # SendGridのアカウント
CONST_PASSWORD = 'XXXXXXXXXX' # SendGridのアカウントのパスワード
CONST_FROM = 'test@test' # SendGrid登録時に申請した送信元ドメインのメールアドレス
CONT_SMTP_AUTH = 'no' # SMTP-AUTHは使わない
# Mailクラス
class MailClass:
_smtpobj = None
def Connect(self,server = CONST_SERVER,port=CONST_PORT,account=CONST_ACCOUNT,password=CONST_PASSWORD):
try:
#デフォルト
if server == '':
server = CONST_SERVER
if port == 0:
port = CONST_PORT
if account == '':
account = CONST_ACCOUNT
if password == '':
password = CONST_PASSWORD
self._smtpobj = smtplib.SMTP(server, port)
self._smtpobj.ehlo()
self._smtpobj.starttls()
self._smtpobj.ehlo()
if CONT_SMTP_AUTH == 'yes':
self._smtpobj.login(account,password)
return True
except (smtplib.SMTPHeloError,
smtplib.SMTPAuthenticationError,
smtplib.SMTPException
) as e:
if self._smtpobj != None:
self._smtpobj.quit()
self._smtpobj = None
return False
def Disconnect(self):
if self._smtpobj != None:
self._smtpobj.close()
def CreateMail(self,to,id,dt,name,user_email,comment):
body = '問合せID:'+str(id)
body += '\n'
body += '日時:'+dt.strftime('%Y-%m-%d %H:%M:%S')
body += '\n'
body += '名前:'+name
body += '\n'
body += 'メールアドレス:'+user_email
body += '\n'
body += '【内容】'
body += '\n'
body += comment
charset = "utf-8"
msg = MIMEText(body, "plain", charset)
msg.replace_header("Content-Transfer-Encoding", "base64")
msg['Subject'] = 'QAが届きました'
msg['From'] = CONST_FROM
msg['To'] = to
send_dt=email.utils.formatdate()
msg['Date'] = send_dt
return msg
def SendMail(self,msg):
try:
self._smtpobj.send_message(msg)
return True
except Exception as e:
return False
def __init__(self):
self._smtpobj = None
def __del__(self):
self._smtpobj = None
if __name__ == '__main__':
Mail = MailClass()
Mail.Connect('',0,'','')
to ='aaa@bbb'
id=1
dt= datetime.datetime.now()
name='test'
email='test@test'
comment='test'
msg=Mail.CreateMail(to,id,dt,name,email,comment)
Mail.SendMail(msg)
Mail.Disconnect()
⑤Python+Flask のWebアプリケーションとApacheとの連結
最後に、 Python+Flask のWebアプリケーションをApacheに連結します。
・ Python+Flask のWebアプリケーション 側の設定
以下のPyhtonコードを記載したファイル(WSGIスクリプト:QA.wsgi)を作成します。
import sys
# WebアプリケーションのPyhton+Flaskのソースコードを保管しているディレクトリのパスを指定
sys.path.insert(0, '/home/swata001/qa')
# QA.pyの中で定義したアプリケーションインスタンス("app")を定義する
from QA import app as application
・Apache側の設定
/etc/apache2/sites-available配下に以下のファイル(flask.conf)を作成します。
これによって、HTTPの80ポートでブラウザからApacheにアクセスし、作成したPaython+FlaskのWebアプリケーションにアクセス可能となります。
このファイルの中で、上記で作成したWSGIスクリプト:QA.wsgiの指定、Pythonの各種パッケージライブラリがインストールされているパスを指定、前述した~/.local/bin/mod_wsgi-express install-moduleの出力結果の追記などをします。
<VirtualHost *:80>
ServerName <ホスト名>.<ドメイン名>
ServerAlias www.<ホスト名>.<ドメイン名>
# Webアプリケーションの格納ディレクトリ
DocumentRoot "/home/swata001/qa"
# WSGIスクリプトの指定
WSGIScriptAlias / "/home/swata001/qa/QA.wsgi"
# 自作したアプリケーション格納先。実行・読み取り権限の指定(すべて許可)
<Directory /home/swata001/qa>
# Apache2.4系と2.2系で、記載が微妙に変わります。(これは2.4)
Require all granted
</Directory>
RewriteEngine on
RewriteCond %{SERVER_NAME} =www.icebreak.itresourcetech.net [OR]
RewriteCond %{SERVER_NAME} =icebreak.itresourcetech.net
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>
# Pythonの各種パッケージライブラリがインストールされているパスを指定する
WSGIPythonPath /home/swata001/anaconda3/lib/python3.8/site-packages
WSGIApplicationGroup %{GLOBAL}
# ~/.local/bin/mod_wsgi-express install-moduleの出力結果をコピペする
LoadModule wsgi_module "/usr/lib/apache2/modules/mod_wsgi-py38.cpython-38-x86_64-linux-gnu.so"
WSGIPythonHome "/home/swata001/anaconda3"
また、HTTPの443ポート(HTTPS)でアクセスする場合、RSAの鍵ペアを作成し、設定する必要があります。
無料のLetsencryptを利用し、RSAの鍵ペアを生成し、Apacheに設定します。
以下の手順を実施します。その際、<ホスト名>.<ドメイン名>を自分のものを指定します。
途中で、認証のために、DNSサーバのTXTレコードを追加する旨の指示がありますので、指定のテキストを記載したTXTレコードをDNSサーバに設定します。(認証が終われば、削除)
「Ubuntu 20.04でLet’s Encryptを使用してApacheを保護する方法」https://www.digitalocean.com/community/tutorials/how-to-secure-apache-with-let-s-encrypt-on-ubuntu-20-04-ja
その後、 再度、 /etc/apache2/sites-available配下に以下のファイル(flask-le-ssl.conf)を作成します。
これによって、HTTPSでブラウザからApacheにアクセスし、作成したPaython+FlaskのWebアプリケーションにアクセス可能となります。
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerName <ホスト名>.<ドメイン名>
ServerAlias www.<ホスト名>.<ドメイン名>
# Webアプリケーションの格納ディレクトリ
DocumentRoot "/home/swata001/qa"
# WSGIスクリプトの指定
WSGIScriptAlias / "/home/swata001/qa/QA.wsgi"
# 自作したアプリケーション格納先。実行・読み取り権限の指定(すべて許可)
<Directory /home/swata001/qa>
# Apache2.4系と2.2系で、記載が微妙に変わります。(これは2.4)
Require all granted
</Directory>
# letsencryptで生成されたRSAの鍵ペアなどのパス
Include /etc/letsencrypt/options-ssl-apache.conf
SSLCertificateFile /etc/letsencrypt/live/icebreak.itresourcetech.net/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/icebreak.itresourcetech.net/privkey.pem
</VirtualHost>
</IfModule>
以下のコマンドで、Apacheを起動します。
sudo systemctl restart apache2
■ ステップ5)GCPのWebアプリケーションにブラウザからアクセス
インターネットを経由して、ブラウザからGCPのWebアプリケーションにアクセスするには、DNSの設定が必要です。
外部のDNSサービスとして、Xserverを利用する場合、Aレコードを以下のように追加設定する必要があります。
<ホスト名>.<ドメイン名> A <GCEのUbuntuの外部IPアドレス>
www. <ホスト名>.<ドメイン名> A <GCEのUbuntuの外部IPアドレス>
注意点としては、 GCEのUbuntuの外部IPアドレス は、VMインスタンスの開始・停止の度に変わります。(エフェメラル)そのため、静的IPアドレスに変更し、固定とする必要があります。
固定IPとするには、GCPのダッシュボードからVPCネットワーク→外部IPアドレスを選択します。
「静的アドレスを予約」をクリックします。
名前、スタンダード、IPv4、タイプ=リージョン、リージョン=us-west1(オレゴン)、接続先=VMインスタンスの名前を入力し、予約をクリックします。
種類が「静的」に変更され、外部アドレスに固定のIPアドレスが付与されます。
同時にVMインスタンスのGCEのUbuntuのIPアドレスも固定の外部IPアドレスに変更されます。
さて、それでは、ブラウザからアクセスしてみます。
http://<ホスト名>.<ドメイン名>/
あるいは
https://<ホスト名>.<ドメイン名>/
のURLをブラウザから入力します。すると以下の画面が表示されます。
名前、メールアドレス、コメントを入力し、「問合わせる」をクリックすると、結果が表示されます。
送信先には、以下のメールが送られます。
以上のようにして、GCPを使って、Python+FlaskのWebアプリケーションを作ることができます。
■ まとめ
ほんの小さなWebサービスでしたが、GCPのクラウド環境上にゼロから立ち上げる手順を体験できたと思います。かなりの手順ですが、他のクラウド環境も大筋は同様なものと思われます。
これらはただの手順であり、Webサービスを動かす手段でしかありません。
本質は、Webサービスのビジネスモデルをしっかり考え、品質を確保した上でサービをリリースすることですのでお忘れなく。
ソフトウェア開発・システム開発業務/セキュリティ関連業務/ネットワーク関連業務/最新技術に関する業務など、「学習力×発想力×達成力×熱意」で技術開発の実現をサポート。お気軽にお問合せ下さい