LoRaWANとSORACOMFunnelのAWSIoTアダプタを使ってDynamoDBにデータを書き込む

はじめに

つい先日、SORACOMFunnelがAWSIoTに対応したというニュースを耳にしました
ちょうど仕事の関係でSORACOMのシールドが届いたし、会社にLoRaWANのPublicGWもあることだし・・・
ということでちょいと触ってみた
SORACOM公式ブログにも手順が書いてありましたが、ちょっと躓いたところがあったりしたので、まとめてみました

やりたいこと

  1. LoRaデバイスからLoRaゲートウェイを通ってAWSIoTにセンサーデータを投げる
  2. AWSIoTが受け取ったデータを加工するためのLambdaファンクションをキックする
  3. Lambdaがデータを加工してDynamoDBに格納する

SORACOM Funnelって?

SORACOM Funnel(以下、Funnel) は、デバイスからのデータを特定のクラウドサービスに直接転送するクラウドリソースアダプターです。
Funnel でサポートされるクラウドサービスと、そのサービスの接続先のリソースを指定するだけで、データを指定のリソースにインプット
することができます。

http://soracom.jp/services/funnel/より抜粋
要するに、デバイスからAWSなどのクラウド上に閉域網でデータを送信することができるサービス(合ってるかな・・・)

AWSIoTって?

AWS IoT によって、さまざまなデバイスを AWS の各種 Services や他のデバイスに接続し、データと通信を保護し、
デバイスデータに対する処理やアクションを実行することが可能になります。
アプリケーションからは、デバイスがオフラインの状態でもデバイスとのやり取りが可能です。

https://aws.amazon.com/jp/iot-platform/how-it-works/より抜粋
うーん、なるほどわからん。とりあえず使ってみよう

デバイス側の設定

同じ部署の電気系強いお方が気づいたらセッティングしていただいていましたので割愛
この時点でSORACOM Harvestにてデータが送信されているのを確認できている状態

AWSIoTの設定

Funnelでデータを送信する先のAWSIoTを作成します

エンドポイントを控える

Funnelを設定する際に必要なAWSIoTのエンドポイントを控えておきます

AWSIoT_TOP.PNG

Ruleを作成する

左のサイドメニューから「Rule」を選択し、「Create a rule」をクリック

AWSIoT_Rule.PNG

「Name」と「Description」を入力する(Descriptionは任意)

AWSIoT_Rule_name.PNG

「Attribute」に「*」、「Topic filter」に「IoTDemo/#」を入力
「Using SQL version」は「2016-03-23」で問題なければそのままでOK

AWSIoT_Rule_massage.PNG

「Set one or more actions」の「add action」をクリック

AWSIoT_Rule_set_action.PNG

今回はLambdaでデコードする必要があるため「Invoke a Lambda function passing the message data」を選択

AWSIoT_Rule_select_lambda.PNG

「Configure action」を選択

AWSIoT_Rule_select_lambda_button.PNG

キックするLambda Functionを選択
今回は初めて作成するので、Lambdaが呼ばれたときのeventの中身をログに吐き出すLambdaを作成して、それをキックするようにします
※DynamoDBに格納する処理は後ほど実装

「Create a new resouce」をクリック。Lambdaのページに遷移します

AWSIoT_Rule_lambda_create.PNG

「Blank Function」を選択

Lambda_create.PNG

Lambdaのトリガーを設定
「IoTタイプ」は「カスタムIoTルール」を選択
「ルール名は」現在作成中のルール名
「SQLステートメント」は作成中の「Rule query statement」の中身をコピー
「次へ」をクリック

Lambda_trigger.PNG

「名前」はお好きなFunction名をつけてください
「ランタイム」は筆者の好みによりNode.jsです
コードには

exports.handler = (event, context, callback) => {
    console.log(event);
};

と書いておいてください。

Lambda_setting.PNG

あとは、DynamoDBの権限を持ったロールを選択(作成)して、ページ下部の「次へ」をクリックしてLambdaFunctionを作成してください

AWSIoTのページに戻って、先ほど作成したLambdaFunctionを選択し、「Add action」をクリック

AWSIoT_Rule_add_lambda.PNG

その後「create Rule」をクリックするとRuleが作成されます
これでAWSIoTのRule作成が完了です

SORACOM Funnelの設定

まず、SORACOMコンソールにログインし、再度メニューから「LoRaグループ」⇒「追加」をクリックします
ポップアップが出てきてグループ名を入力するように言ってくるので、任意のグループ名を入力しグループを作成します

作成したグループを選択し、設定画面に移動します

転送先サービス:AWS IoT
転送先URL:https:///rule内で作成したSQLTopicFilter/#{deviceId}
認証情報:AWSIoTの権限を持ったIAMアカウント情報で作成したもの
送信データ形式:無難にJSON

funnel_setting.PNG

※転送先URLにはプレースホルダーを作成することができます
  - SIMを利用する場合:{imsi}
  - LoRaデバイスを利用する場合:{deviceId}

これでFunnelの設定は完了です

Lambdaの実装

デバイスの電源を入れ、データが送信されるようになると、Lambdaが起動してeventの中身をログに吐き出していると思います
↓こんな感じ

2017-06-23T04:13:59.850Z 62014535-57ca-11e7-b4e4-9fbd147f2037 { 
  operatorId: '0123456789',
  timestamp: 1498191237793,
  destination: { 
    resourceUrl: 'https://xxxxxxxxx.iot.ap-northeast-1.amazonaws.com/xxxxxxx/#{deviceId}',
    service: 'aws-iot',
    provider: 'aws' 
  },
  credentialsId: 'iot-sys',
  payloads: { 
    date: '2017-06-23T04:13:54.276320',
    gatewayData: [ [Object] ],
    data: '7b2268223a36312e367d',
    deveui: '1234567890' 
  },
  sourceProtocol: 'lora',
  deviceId: '1234567890' 
}

センサーから送られてくるデータはevent[“payloads”][“data”]にHEX形式で格納されているので、取り出してデコードする必要があります。


const data = event["payloads"]["data"];
const decodeData = new Buffer(data, "hex").toString("utf8");

デコードすると「7b2268223a36312e367d」⇒「{“h”: 61.6}」のようなString型になります(これは一例)

Object型のほうが使い勝手がよいので、parseしてしまいましょう


const parseData = JSON.parse(decodeData); // {h : 61.6}

あとはDynamoDBにputで投げつけます

index.js
"use strict";

const AWS = require("aws-sdk");
const co = require("co");
const moment = require("moment-timezone");

const dynamodb = new AWS.DynamoDB.DocumentClient({
  region: "ap-northeast-1"
});

const dynamoPutData = require("./lib/dynamo_put_data");

exports.handler = (event, context, callback) => {
  // UTCなのでJSTに変換
  const date = event["payloads"]["date"];
  const time = moment(date).tz("Asia/Tokyo").format();
  // HEX形式をデコード
  const data = event["payloads"]["data"];
  const decodeData = new Buffer(data, "hex").toString("utf8");
  // Object型に変換
  const parseData = JSON.parse(decodeData);
  // deviceIdを取得
  const deviceId = event["deviceId"];

  // DynamoDBにPUTするItem
  const item = [{
    deviceId: deviceId,
    time: time,
    value: parseData
  }];

  co(function *() {
    yield dynamoPutData.putDynamoDB(dynamodb, item[0]);
  }).then(() => {
    console.log("success!")
  }).catch((err) => {
    console.log(err);
  });
};

dynamo_put_data.js
"use strict";

class dynamoPutData {
  /**
   * DynamoDBへのPUT処理
   * @param {DocumentClient} dynamoDB
   * @param item
   * @returns {Promise}
   */
  static putDynamoDB(dynamoDB, item) {
    const params = {
      TableName: "TABLE_NAME",
      Item: item
    };
    return dynamoDB.put(params).promise();
  }
}

module.exports = dynamoPutData;

dynamo_put_data.js中の”TABLE_NAME”にはデータを投げつけるテーブル名を書いてください
関数を外だしして複数ファイルがあるので、Lambdaにはソースコード一式をZIPに固めてアップする方法でデプロイを行います
データが送られてきてLambdaがキックされると、DynamoDBにデータが格納されていると思います

まとめ

日ごろからAWSのサービスを使っていましたが、AWSIoTを利用する機会がなくとてもいい経験になりました。
今回はデバイスからクラウドといった方向でしたが、AWSIoTを利用すればその逆方向も実現することができるらしいので、近々そういった実装もしてみたと思います

では!

続きを読む

CodeStarバンザイ!数クリックで始めるCI/CDパイプライン

CodeStarを使ってCI/CDパイプラインを構築してみたので、紹介します。

CodeStarとは?

AWSのマネージドサービスであるCodePipeline、CodeCommit、CodeBuild、CodeDeployを使ってCI/CDパイプライン+実行環境をさくっと構築してくれるサービスらしい。

https://aws.amazon.com/jp/codestar/

よしっ、動かしてみるぞ!!

1.プロジェクト作成

マネジメントコンソールからCodeStarを選び、「start a project」をポチッ。
するとプロジェクトのテンプレートを選択する画面が表示される。

カテゴリは、Web application、Web service、Alexa Skill、Static Websiteから、
言語は、Ruby、Node.js、Java、Python、PHP、HTML 5から、
実行環境は、Beanstalk、EC2、Lambdaから選択できる。
※もちろん存在しない組み合わせもあります。

今の時代っぽいWeb Application×Node.js×Lambdaなんてのも選択できるんですね。

うーん、ここはCodeBuildも使ってみたいし「Web Application×Java Spring×EC2」を選択。

使う1.png

そして、プロジェクト名を入力。インスタンスタイプを設定して。ポチッ、ポチッ。

使う2.png

。。。
はいっ、CI/CDパイプライン構築の作業はこれで終わり。

そして、待つこと10分。
CodePipeline、CodeCommit、CodeBuild、CodeDeployと、これを統合的に確認するためのダッシュボードがいい感じにできちゃいました。
もちろんjavaのwebアプリケーションも起動しています。

ダッシュボード

使う4修正.png

CodePipeline

使う8.png

CodeCommit

使う5.png

CodeBuild

使う6.png

CodeDeploy

使う7.png

デプロイされたjavaのwebアプリケーション

12.png

2.CI/CDパイプライン実行

ここからが本番。
gitへの接続情報をIAMで確認(ユーザ⇒認証情報⇒AWS CodeCommit の HTTPS Git 認証情報「生成」)し、code commit上のソースコードをcloneする。
するとこんなものが落ちてきます。

tree
.
├── README.md
├── appspec.yml
├── buildspec.yml
├── pom.xml
├── scripts
│   ├── install_dependencies
│   └── start_server
└── src
    └── main
        ├── java
        │   └── com
        │       └── aws
        │           └── codestar
        │               └── projecttemplates
        │                   ├── HelloWorldAppInitializer.java
        │                   ├── configuration
        │                   │   ├── ApplicationConfig.java
        │                   │   └── MvcConfig.java
        │                   └── controller
        │                       └── HelloWorldController.java
        ├── resources
        │   └── application.properties
        └── webapp
            ├── WEB-INF
            │   └── views
            │       └── index.jsp
            └── resources
                ├── gradients.css
                ├── set-background.js
                ├── styles.css
                └── tweet.svg

ふむふむ、なるほど。
ここは手っ取り早くCI/CDパイプラインを確認するため、index.jspをちょこっと修正。
そして、code commitにpush。
code commit上で、変更した内容も確認できます。

使う11.png
すると。。。

使う9.png

パイプラインが動き出したーーー
どうやら動きとしては、こんなことをやっているみたい。

  • Source : 新たしいコードがpushされると、code commitからソースコードを取得しS3に格納。
  • Build : S3からソースコードを取得しビルド。そしてビルドしたモジュールをS3に格納。
  • Application : S3に格納されたモジュールをEC2にデプロイ。

待つこと5分。デプロイまで成功。
そして、先程の画面を確認してみると。。。

使う10.png

変わった!
簡単!!

これを使えば

  • Java、Rubyなどメジャーな言語を利用したCI/CDパイプラインを爆速で構築できる。
  • Jenkinsから開放される。

がしかし。。

  • 東京リージョンにはまだ来ていない。
  • CodeStarというかcode commit側の問題になるが、pull requestが使えない。。

本番用のアプリケーション開発環境・実行環境として利用するのは、まだまだ難しいような気もしますが、
pocくらいであればこれで十分かもしれませんね。

続きを読む

LambdaでAWSの料金を毎日Slackに通知する(Python3)

はじめに

個人アカウントは基本的に無料枠で運用しているので、少しでも請求がある場合はいち早く気づきたいです。
先日、とあるハンズオンイベントで使ったリソースを消し忘れて、最終的に$10ぐらい請求が来てしまいました。。。

CloudWatchで請求アラートは設定していますが、閾値超えが既知の場合、当然見逃すため、最終的な請求額に驚くハメになります。

これを防ぐためにLambdaで毎日SlackにAWS料金を通知することにします。

先日LambdaがPython3に対応したので、せっかくだし勉強がてらPython3で実装したい。
ネット上にはNode.jsでの実装例が多いようで、今回はこちらを参考にPython3で実装してみます。

必要なもの

  • Slack

    • incoming-webhooks URL

    • 適当なchannel
  • lambda-uploader
    • requestsモジュールをLambda上でimportするために利用

      • カレントディレクトリにモジュールをインストールして、モジュールごとZipに固めてアップロードでもいけるはずですが、私の環境だとうまくいかなかったので
  • aws cli
    • lambda-uploaderで必要
  • AWS
    • Lambda関数用IAM Role

      • CloudWatchReadOnlyAccessポリシーをアタッチ

事前準備

lambda-uploaderをインストール

$ pip install lambda-uploader 

こちらを参考にさせていただきました。

aws cliをインストール

$ pip install awscli

credential、リージョン設定

$ aws configure

確認
$ aws configure list

コード

ディレクトリ構成

ディレクトリ名は任意です。関数名とは無関係です。

ディレクトリ構成
awscost_to_slack/
|--lambda_function.py
|--requirements.txt
|--lambda.json

lambda_function.py

超過金額に応じて色をつけるようにしています。
\$0.0なら緑、超過したら黄色、\$10超えで赤になります。

lambda_function.py
#!/usr/bin/env python
# encoding: utf-8

import json
import datetime
import requests
import boto3
import os
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

# Slack の設定
SLACK_POST_URL = os.environ['slackPostURL']
SLACK_CHANNEL = os.environ['slackChannel']

response = boto3.client('cloudwatch', region_name='us-east-1')

get_metric_statistics = response.get_metric_statistics(
    Namespace='AWS/Billing',
    MetricName='EstimatedCharges',
    Dimensions=[
        {
            'Name': 'Currency',
            'Value': 'USD'
        }
    ],
    StartTime=datetime.datetime.today() - datetime.timedelta(days=1),
    EndTime=datetime.datetime.today(),
    Period=86400,
    Statistics=['Maximum'])

cost = get_metric_statistics['Datapoints'][0]['Maximum']
date = get_metric_statistics['Datapoints'][0]['Timestamp'].strftime('%Y年%m月%d日')

def build_message(cost):
    if float(cost) >= 10.0:
        color = "#ff0000" #red
    elif float(cost) > 0.0:
        color = "warning" #yellow
    else:
        color = "good"    #green

    text = "%sまでのAWSの料金は、$%sです。" % (date, cost)

    atachements = {"text":text,"color":color}
    return atachements

def lambda_handler(event, context):
    content = build_message(cost)

    # SlackにPOSTする内容をセット
    slack_message = {
        'channel': SLACK_CHANNEL,
        "attachments": [content],
    }

    # SlackにPOST
    try:
        req = requests.post(SLACK_POST_URL, data=json.dumps(slack_message))
        logger.info("Message posted to %s", slack_message['channel'])
    except requests.exceptions.RequestException as e:
        logger.error("Request failed: %s", e)

requirements.txt

pip installしたいモジュール名を書きます。

requirements.txt
requests

lambda.json

Lambda関数名、IAM RoleのARNなどは環境に合わせてください。
スクリプト本体のファイル名とハンドラの前半を一致させないと動きません。地味にハマるので注意!

lambda.json
{
  "name": "LAMBDA_FUNCTION_NAME",
  "description": "DESCRIPTION",
  "region": "ap-northeast-1",
  "handler": "lambda_function.lambda_handler",
  "role": "arn:aws:iam::XXXXXXX:role/ROLE_NAME_FOR_LUMBDA",
  "timeout": 300,
  "memory": 128
}

デプロイ

上記ファイルを配置したディレクトリに移動して、lambda-uploaderを実行します。

$ cd awscost_to_slack
$ lambda-uploader
λ Building Package
λ Uploading Package
λ Fin

Lambda環境変数設定

今回のLambda関数では、通知先SlackチャネルとWebhooks URLを環境変数で渡すようにしたので、設定します。

スクリーンショット 2017-06-23 14.51.30.png

lambda-uploaderのlambda.jsonに書けそうなのですが、書式が分からず、今回はマネコンで設定しました。
lambda-uploaderでLambda関数を更新すると消えてしまうので注意。

Lambda定期実行設定

CloudWatchのスケジュールイベントを定義して、lambda関数をターゲットに指定します。
時刻はUTCなので注意しましょう。
毎日UTC0:00に実行されるよう設定しました。

スクリーンショット 2017-06-23 15.01.27.png

実行イメージ

スクリーンショット 2017-06-23 14.15.54.png
こんな感じで毎朝9:00に通知がきます。

まとめ

Lambdaはほぼ無料でプログラムが動かせるので楽しいですね!
Python初心者なのでコードが見苦しい点はご容赦ください。

続きを読む

AWS LambdaとAPIGatewayのミニマム構築

サーバレスでweb apiを構築できるLambdaとAPIGateway。
これをミニマムな構成で構築してみる

とはいえ認証もなにもない状態だとアレなのでAPI keyを発行した認証は実装する

Lambda

aws consoleからLambdaに入り、Node.jsの6.10を選択、Blank Functionを選択

今回はNodeを選択しているけど、もちろんPythonでもいい

Kobito.XZSrgA.png

ひとまずトリガーはとりあえず作らないで次へ

Kobito.FgA1X1.png

Lambdaの名前を入力し、コードを入力する。
今回はコードはデフォルトのままで進める。

Kobito.R1y4r2.png

スクロールするとさらに設定画面が出てくる
ロール割当だけすれば最小限OK

Kobito.1G5h7o.png

これでLambda関数の作成はOK

API Gateway

APIを定義

aws consoleからAPI gatewayを開く

新しいAPI を選択し、適当な名前を入力

Kobito.46WF6B.png

リソース -> アクション -> メソッドの作成を選択

Kobito.hdwTH2.png

メソッドにPOSTを追加し、Lambda関数を追加する

Kobito.qTpcmT.png

なんかフローみたいのが出てくる。

メソッドリクエストを選択

Kobito.HPvhHq.png

APIキーの必要性にtrueを設定

Kobito.ZD35fr.png

アクション -> デプロイを選択

デプロイするステージを選択(なければ作る)してデプロイ実行

Kobito.tepBZg.png

APIキーの作成

APIキーを選択 -> アクション -> APIキーの作成

Kobito.ilVIPU.png

APIキーの名前、説明を入力。
対象のAPIとステージも一緒に選択。
ここでランダムな文字列のAPIキーが発行されるのでメモしておく(API実行時に必要になる)

Kobito.Xsx7CS.png

使用プランの作成

使用プランを選択 -> 作成を選択

適当な値を入力して使用プランを作成

Kobito.0kronG.png

使用プランにAPIキーを割り当てる
作成したAPIキーをセットする

Kobito.3S94mx.png

実行テスト

postman等からAPIのURLを叩く

headerに x-api-key という名前でAPIkeyを設定すると認証が通り、実行される(はず)

Kobito.IGiR2w.png

続きを読む

AWS独学メモ

頑張って学んでいきます。

サービス俯瞰

コンピューティング関連

サービス名 概要
EC2 仮想サーバー
EC2 Container Service Doker(アプリ実行環境構築ツール)運用サービス
EC2 Container Regstry Dokerイメージ保存・共有サービス。
Elastic Beanstalk .NET/PHP/Python/Ruby/Node.jsアプリを自動でAWSにデプロイ。
Lambda クライアントからのリクエスト発生時に任意プログラミング起動。イベント駆動型サービス。
Auto Scaling CPU使用率等、事前決定条件に応じ、EC2インスタンス増減
Elastic Load Balancing トラフィックに応じ、複数EC2インスタンスに負荷分散

ストレージ・コンテンツ配信

サービス名 概要
S3 ファイルサーバ。画像格納したり。
CloudFront コンテンツ配信ネットワーク。利用者から近い場所から効率よく配信
EBS EC2データを保持するストレージ。EC2のHDD,SSDのような役割。
Elastic File System EC2共有ファイルストレージ
Glacier 低価格ストレージ。仕様頻度低いけど長期保存のバックアップ用。
Import / Export Snowball ペタバイト級の大容量転送サービス。
Storage Gateway オンプレミスとAWSを接続

DB関連

サービス名 概要
RDS DB(MySQL/Oracle/SQL Server/PostgreSQL/Aurora)が利用できる
Database Migration Service 最小限停止時間でDBを移行。オンプレミスのDBサーバからの移行等に用いる
DynamoDB NoSQLデータベスサービス構築/運用。
ElastiCache クラウドでのメモり内キャッシュの管理サービス
Redshift ビッグデータを分析

ネットワーク

サービス名 概要
VPC プライベートネットワーク構築サービス。
Direct Connect オンプレミスのネットワークとAWSのVPCネットワークを直接接続。
Route 53 DNS(ドメイン名とIPアドレスを対応)

開発者用ツール

サービス名 概要
CodeCommit プライベートGit
CodeDeploy 開発アプリを実行環境に自動配置
CodePipeline 継続的デリバリ使用したアプリのリリース

開発ツール

サービス名 概要
CloudWatch AWSリソース監視サービス
CloudFormation テンプレート利用したリソースの作成と管理
CloudTrail ユーザアクティビティとAPI使用状況確認
Config リソースのイベントリ変更の追跡
OpsWorks Chef利用し操作の自動化
Service Catalog 標準化製品の作成と使用
Trusted Advisor パフォーマンスとせきゅりてぃの最適化

セキュリティ

サービス名 概要
IAM AWS認証
Directory Service Active Directoryのホスティングと管理
Inspector アプリのセキュリティ分析
CloudHSM 暗号鍵管理の専用ハードウェア
Key Management Service 暗号鍵作成と管理
WAF 攻撃から保護するファイアウォール

分析

サービス名 概要
EMR Hadoopフレームワーク
Data Pipeline オーケストレーションサービス
Kinesis リアルタイムストリーミングデータとの連携
Machine Learning 機械学習
QuickSight 高速ビジネスインテリジェンスサービス

モバイルサービス

サービス名 概要
Mobile Hub モバイルアプリの構築/テスト/監視
API Gateway RESTful APIの構築/管理
Cofnito ユーザID及びアプリデータの同期
Device Farm iOS/Android/FireOSアプリのテスト
Mobile Analytics アプリ分析の収集/表示/エクスポート
Mobile SDK モバイルソフトウェアの開発キット

アプリケーションサービス

サービス名 概要
AppStream ストリーミングサービス
CloudSearch マネージド型検索サービス
Elastic Transcorder メディアと動画変換
SES Eメール送受信
SNS プッシュ通知サービス
SQS メッセージキューサービス
SWF アプリ同士を連携ワークフローサービス

大企業向け

サービス名 概要
WorkSpaces クラウド上仮想デスクトップパソコンサービス
WorkMail セキュリティ保護、企業向けEメール及びカレンダー
WorkDocs ファイル共有サービス

S3について

用語

用語 意味
バケット データの入れ物
オブジェクト 格納ファイル

ステップ

  1. バケット作成
  2. オブジェクト格納

EC2について

用語

用語 意味
EC2 仮想サーバ。オンプレミスのWindowsサーバやUNIXサーバに相当。
インスタンス 1台の仮想サーバ
EBS(Elastic Block Store) サーバのHDDに相当する仮想ディスク
AMI(Amazon Machine Image) サーバにインストールするOSやミドルウェアやアプリのイメージ。新インスタンスを複数生成時、AMIを利用。
yum パッケージ管理システム
scp(secure copy) SSH機能を用いて、安全にファイル転送する

EC2にSSH接続した

参考ページ1
参考ページ2

ミドルウェアをインストール

yum更新
$ sudo yum -y update
httpdインストール
$ sudo yum  install -y httpd
httpd起動
$ sudo service httpd start
httpd自動起動を確認
$ sudo chkconfig --list httpd
httpd自動起動を設定
$ sudo chkconfig  httpd on
$ sudo chkconfig  httpd off

scp(コンテンツをアップロードする)

【現在ここで躓き中!】
→ 突破!!

参考ページ1

HTTPコンテンツをコピー

HTTPコンテンツのコピー

$ sudo cp /home/ec2-user/index.html /var/www/html/

【現在ここで躓き中!】index.htmlへアクセスできない

続きを読む