20200928のAWSに関する記事は15件です。

何も考えずにEC2にDjangoプロジェクトを展開してブラウザから管理者画面を表示するまでのメモ

はじめに

AWSのEC2上でDjangoプロジェクトを展開しようとして丸一日悩んでしまったので、手順をまとめました。あくまでも自己責任でお願いします。

やったこと

EC2インスタンスを立てて何も考えずにDjangoを動かして、ネット上からアクセスできるようにする。

注意

  • ec2-userをそのまま使っていたり、セキュリティ的に怪しい所があるため注意すること。今回はAWSでdjangoプロジェクトをとりあえず動かせるようにすることを目的とする。
  • VPC、サブネット、インターネットゲートウェイの設定はすでに完了していて、EC2自体はネットから参照できる状態になっている所からスタートする
  • ssh(22)、HTTP(80)とDjango用(8000)のポートが開放されている状態を前提とする
  • DBはSQLite3を使用する。また、SQLite3のバージョンを最新にするため既存のソフトウェアでSQLite3を使用している際は注意すること。
  • Djangoの管理者画面がスタイルシートなどを含めて読み込めたところが本記事のゴールとする

参考にした文献

必要なものを揃える

本記事ではvenvなどは使わず、そのままpython3を使っていくため注意すること。

install
$ sudo yum update
$ sudo yum install python3
# Amazon Linux 2で$sudo yum install nginx と入力するとコマンドを催促されるので、それを入力する。
$ sudo amazon-linux-extras install nginx1
$ sudo pip3 install django
$ sudo pip3 install gunicorn

Amazon Linux 2にもデフォルトでSQLite3がインストールされていたが、Djangoで求めるバージョン(3.8.3以降)にマッチしないため、SQLiteのアプデを実施する。

SQLite3のinstall
# 後述のSQLite3インストール時のmakeファイルのビルドに使用する。
$ sudo yum install gcc
# 最新版のSQLite3をインストールする。執筆当時は3.33.0が最新だった。(https://www.sqlite.org/download.html)
# 展開した後にmakeファイルをビルドしてmake installまで実施する
$ wget https://www.sqlite.org/2020/sqlite-autoconf-3330000.tar.gz
$ tar zxvf sqlite-autoconf-3330000.tar.gz
$ cd sqlite-autoconf-3330000
# makeファイルのビルド
$ ./configure --prefix=/usr/local
$ sudo make
$ sudo make install

次に、過去に入っていたsqlite3から現行のsqlite3を使用するように設定を通す。

SQLite3のinstallつづき
# すでにインストール済みのsqlite3を確認する
$ sudo find / -name sqlite3
# もともとあったやつは多分これ。バージョンが古いことを一応確認しておく。
/usr/bin/sqlite3 --version
# 今回インストールしたのは多分これ。バージョンが古いことを一応確認しておく。
/usr/local/bin/sqlite3 --version
# 古い方のsqlite3のフォルダ名を変え、また新しい方のsqlite3へパスを設定しておく
sudo mv /usr/bin/sqlite3 /usr/bin/sqlite3_old
sudo ln -s /usr/local/bin/sqlite3 /usr/bin/sqlite3
# ライブラリへのパスを通しておく
sudo touch ld.so.conf/sqlite3.conf
sudo echo /usr/local/lib >> sqlite3.conf
# 過去のsqlite3へのリンクとディレクトリを削除
$ sudo rm /lib64/libsqlite3.so.0
$ sudo rm -rf /lib64/libsqlite3.so.[過去のバージョン]
# ライブラリのパスを更新する
sudo ldconfig
# 新しいライブラリへのパス(/usr/local/lib)が通っているか確認する
$ ldconfig -p | grep sqlite
# 一応バージョンも確認する
sqlite3 --version

最後のバージョン確認で現行のSQlite3のものが出力されれば、次に進む。

Djangoの設定

とりあえずプロジェクトを作る。プロジェクト名は公式ドキュメントでおなじみのmysiteにする。

django-admin
# /home/ec2-userなどの任意のユーザのhomeディレクトリへ移動
$ cd /~
$ django-admin startproject mysite

今回はadmin画面が見えればOKなので、最低限の設定だけをする。

django-admin
$ cd mysite/mysite
$ vi settings.py

setting.pyにおいて設定するのは

  • 末尾にSTATIC_ROOTを追加(あわせてimport osも追記)
  • ALLOWED_HOSTSに自分のパブリックドメインやパブリックIPアドレスを設定

ぐらい。一応全文を載せておく

setting.py
import os
from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.                                                                                        
BASE_DIR = Path(__file__).resolve().parent.parent

# SECURITY WARNING: keep the secret key used in production secret!                                                                                      
SECRET_KEY = 'hdxr$tlb=r4ns*#rg806lr*=(aa=_gny4k(1tm9n0atev0n5hs'

# SECURITY WARNING: don't run with debug turned on in production!                                                                                       
DEBUG = True

# 自分のドメインやパブリックIPアドレスとか
ALLOWED_HOSTS = ['*.*.*.*',]


# Application definition                                                                                                                                

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'mysite.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'mysite.wsgi.application'

# Database                                                                                                                                              
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases                                                                                         

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}


# Password validation                                                                                                                                   
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators                                                                          

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization                                                                                                                                  
# https://docs.djangoproject.com/en/3.1/topics/i18n/                                                                                                    

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)                                                                                                                
# https://docs.djangoproject.com/en/3.1/howto/static-files/                                                                                             

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR,'static')

終わったら、djangoプロジェクトの静的ファイルをまとめたり、
管理者ページに入るまでの設定をしておく。

django
$ cd ~/mysite
# 自身の情報をここで入れておく。パスは忘れないように
$ python3 manage.py createsuperuser
# STATICファイルをとりまとめておく
$ python3 manage.py collectstatic

もし、python3 manage.py collectstaticでcheck_sqlite_versionと書かれたエラーを吐いた場合はSQLite3の設定を見直すこと。過去のSQLite3を見に行っている可能性が高い。

Djangoそのものの設定がうまくできているか確認するために、gunicornから動かしてみる。

django
# 必ずmanage.pyなどが存在しているフォルダへ移動すること
$ cd ~/mysite
$ sudo gunicorn mysite.wsgi --bind=0.0.0.0:8000

ブラウザでパブリックIPあるいはドメインへアクセスした際に、例のロケットが表示されればOK。確認できたらgunicornをCtrl+Xなどで終了する。

gunicornの設定

以下の設定ファイルを作成(編集)する。

  • /etc/systemd/system/gunicorn.service
  • /etc/systemd/system/gunicorn.socket

なお、gunicorn.serviceのExecStartのパスはwhichコマンドで確認する。

gunicorn.serviceのExecStartに設定するパスのチェック
which gunicorn

ユーザー名(グループ名)、WorkingDirectory、ExecStartのwsgiファイル名とListenStreamは各自の環境に揃えておくこと。(特に工夫がなければ、ユーザー名はnginxと一致させておく。)

gunicorn.service
[Unit]
Description=gunicorn daemon
Requires=gunicorn.socket
After=network.target

[Service]
Type=notify
# the specific user that our service will run as
# ここのユーザ名とグループ名はデーモンを実行するユーザを入れる。
# 特に何も考えていなければec2-userにする。
User=ec2-user
Group=ec2-user
# another option for an even more restricted service is
# DynamicUser=yes
# see http://0pointer.net/blog/dynamic-users-with-systemd.html
RuntimeDirectory=gunicorn
# Djangoプロジェクトのパスを入れる
WorkingDirectory=/home/ec2-user/mysite
# gunicornの実行ファイルのあるパスと、プロジェクト名.wsgi
ExecStart=/usr/local/bin/gunicorn mysite.wsgi
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true

[Install]
WantedBy=multi-user.target
gunicorn.socket
[Unit]
Description=gunicorn socket

[Socket]
# sockファイルを作る場所を入れる。基本はdjangoプロジェクトのパスを入れておく。
ListenStream=/home/ec2-user/mysite/mysite.sock
# Our service won't need permissions for the socket, since it
# inherits the file descriptor by socket activation
# only the nginx daemon will need access to the socket
# gunicorn.serviceと同じユーザを入れる
SocketUser=ec2-user
# Optionally restrict the socket permissions even more.
# SocketMode=600

[Install]
WantedBy=sockets.target

ここで自分が間違えたポイントとして、sockファイルとsocketファイルを混同させないようにすること。sockファイルはnginxとgunicornがやり取りする際に使うファイルである。

終わったら、gunicornのデーモンの起動と自動起動の登録をする。

gunicorn起動と自動起動設定
$ sudo systemctl start gunicorn.service
$ sudo systemctl enable gunicorn
# ちゃんとプロセスが動いていることを確認する。
$ sudo systemctl status gunicorn

プロセスが正常に起動していれば、次に進む。

nginxの設定

まず、nginxが正常に動いていて、ネットからつながることを確認する。つながらない場合は、EC2ダッシュボードのインスタンス→セキュリティからポート80が開放されているか、あるいはEC2のダッシュボード上で示されているパブリックIPアドレスやドメインが正しいか確認する。

nginx
systemctl start nginx
# コマンド実行後にEC2のパブリックIPアドレスにブラウザからアクセスして、
# nginxが動いているページが見えることを確認する。
# 確認できたら、以下のコマンドで一旦終了する。
systemctl stop nginx

確認できたら、nginxの設定をする。設定するファイルは次の通り。(mysite.confは新規作成する。また、拡張子が.confなら好きな名前でOK)

  • /etc/nginx/nginx.conf
  • /etc/nginx/conf.d/mysite.conf
nginx.conf
# 全文は多すぎるので省略。userをnginxからec2-userなどのサービスを動かすユーザーへ変える。
# 思い入れがなければ、gunicorn起動時のユーザと一致させておく
...
user ec2-user;
...
# httpブロック内などに、この一行があるか確認する。書いてあればOK
include /etc/nginx/conf.d/*.conf;
...
mysite.conf
server {
    listen  80;
    # ドメインあるいはIPアドレスを設定する
    server_name     *.*.*.*;

    # djangoの静的ファイルの置き場所を指定する。
    location /static {
        alias /home/ec2-user/mysite/static;
    }

    # djangoのadmin用の静的ファイルの置き場所を指定する。
    location /static/admin {
        alias /home/ec2-user/mysite/static/admin;
    }

    # gunicorn起動時に作成されるsockファイルの場所を指定する。
    location / {
        proxy_pass http://unix:/home/ec2-user/mysite/mysite.sock;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

}

作成したら、デーモンをstart(あるいはrestart)させる。statusでプロセスが問題なく起動していれば次に進む。

nginx
sudo systemctl start nginx
# すでに起動していた場合はsudo systemctl restart nginx
# プロセスが適切に動いていることを確認する
sudo systemctl status nginx

ブラウザから管理者画面を確認してみる

以下コマンドでnginxとgunicornのプロセスがバックグラウンドで動いていることを確認する。動いていなければエラーから各設定を修正してstartしておく。

プロセスのチェック
$ sudo systemctl status nginx
$ sudo systemctl status gunicorn

そのあと、自身のDjangoプロジェクトフォルダ(/home/ec2-user/mysite)内にmysite.sockが作成されていることを確認し、ブラウザで「自身のパブリックIPアドレスorドメイン/admin」で管理者画面を開いて見る。CSSを含めてAdminのログインページが表示されれば完了。

もしCSSが死んでいる場合はDjangoの設定でSTATIC_ROOTを忘れていたり、mysite.confのLocationやAliasを疑うといいかも。

また、もし502エラー(Bad Gateway)が出てくるようであれば、nginx.confやgunicorn.serviceのユーザーが同じではなかった(意図した権限を持ったユーザじゃなかった)という点や、同ファイルにおいてsockファイルへのパスが異なっている可能性を疑ってみてもいいかも。

今日はここまで。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

何も考えずにEC2にDjangoプロジェクトを展開してブラウザから管理者画面を表示する

はじめに

AWSのEC2上でDjangoプロジェクトを展開しようとして丸一日悩んでしまったので、手順をまとめました。あくまでも自己責任でお願いします。

やったこと

EC2インスタンスを立てて何も考えずにDjangoを動かして、ネット上からアクセスできるようにする。
今回はSQLite3,nginx,gunicornを使います。

注意

  • ec2-userをそのまま使っていたり、セキュリティ的に怪しい所があるため注意すること。今回はAWSでdjangoプロジェクトをとりあえず動かせるようにすることを目的とする。
  • VPC、サブネット、インターネットゲートウェイの設定はすでに完了していて、EC2自体はネットから参照できる状態になっている所からスタートする
  • ssh(22)、HTTP(80)とDjango用(8000)のポートが開放されている状態を前提とする
  • DBはSQLite3を使用する。また、SQLite3のバージョンを最新にするため既存のソフトウェアでSQLite3を使用している際は注意すること。
  • Djangoの管理者画面がスタイルシートなどを含めて読み込めたところで本記事のゴールとする

参考にした文献

必要なものを揃える

本記事ではvenvなどは使わず、そのままpython3を使っていくため注意すること。

install
$ sudo yum update
$ sudo yum install python3
# Amazon Linux 2で$sudo yum install nginx と入力するとコマンドを催促されるので、それを入力する。
$ sudo amazon-linux-extras install nginx1
$ sudo pip3 install django
$ sudo pip3 install gunicorn

Amazon Linux 2にもデフォルトでSQLite3がインストールされていたが、Djangoで求めるバージョン(3.8.3以降)にマッチしないため、SQLiteのアプデを実施する。

SQLite3のinstall
# 後述のSQLite3インストール時のmakeファイルのビルドに使用する。
$ sudo yum install gcc
# 最新版のSQLite3をインストールする。執筆当時は3.33.0が最新だった。(https://www.sqlite.org/download.html)
# 展開した後にmakeファイルをビルドしてmake installまで実施する
$ wget https://www.sqlite.org/2020/sqlite-autoconf-3330000.tar.gz
$ tar zxvf sqlite-autoconf-3330000.tar.gz
$ cd sqlite-autoconf-3330000
# makeファイルのビルド
$ ./configure --prefix=/usr/local
$ sudo make
$ sudo make install

次に、過去に入っていたSQLite3から現行のSQLite3を使用するように設定を通す。

SQLite3のinstallつづき
# すでにインストール済みのsqlite3を確認する
$ sudo find / -name sqlite3
# もともとあったやつは多分これ。バージョンが古いことを一応確認しておく。
/usr/bin/sqlite3 --version
# 今回インストールしたのは多分これ。バージョンが古いことを一応確認しておく。
/usr/local/bin/sqlite3 --version
# 古い方のsqlite3のフォルダ名を変え、また新しい方のsqlite3へパスを設定しておく
sudo mv /usr/bin/sqlite3 /usr/bin/sqlite3_old
sudo ln -s /usr/local/bin/sqlite3 /usr/bin/sqlite3
# ライブラリへのパスを通しておく
sudo touch ld.so.conf/sqlite3.conf
sudo echo /usr/local/lib >> sqlite3.conf
# 過去のsqlite3へのリンクとディレクトリを削除
$ sudo rm /lib64/libsqlite3.so.0
$ sudo rm -rf /lib64/libsqlite3.so.[過去のバージョン]
# ライブラリのパスを更新する
sudo ldconfig
# 新しいライブラリへのパス(/usr/local/lib)が通っているか確認する
$ ldconfig -p | grep sqlite
# 一応バージョンも確認する
sqlite3 --version

最後のバージョン確認で現行のSQlite3のものが出力されれば、次に進む。

Djangoの設定

とりあえずプロジェクトを作る。プロジェクト名は公式ドキュメントでおなじみのmysiteにする。

django-admin
# /home/ec2-userなどの任意のユーザのhomeディレクトリへ移動
$ cd /~
$ django-admin startproject mysite

今回はadmin画面が見えればOKなので、最低限の設定だけをする。

django-admin
$ cd mysite/mysite
$ vi settings.py

setting.pyにおいて設定するのは

  • 末尾にSTATIC_ROOTを追加(あわせてimport osも追記)
  • ALLOWED_HOSTSに自分のパブリックドメインやパブリックIPアドレスを設定

ぐらい。一応全文を載せておく

setting.py
import os
from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.                                                                                        
BASE_DIR = Path(__file__).resolve().parent.parent

# SECURITY WARNING: keep the secret key used in production secret!                                                                                      
SECRET_KEY = 'hdxr$tlb=r4ns*#rg806lr*=(aa=_gny4k(1tm9n0atev0n5hs'

# SECURITY WARNING: don't run with debug turned on in production!                                                                                       
DEBUG = True

# 自分のドメインやパブリックIPアドレスとか
ALLOWED_HOSTS = ['*.*.*.*',]


# Application definition                                                                                                                                

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'mysite.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'mysite.wsgi.application'

# Database                                                                                                                                              
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases                                                                                         

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}


# Password validation                                                                                                                                   
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators                                                                          

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization                                                                                                                                  
# https://docs.djangoproject.com/en/3.1/topics/i18n/                                                                                                    

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)                                                                                                                
# https://docs.djangoproject.com/en/3.1/howto/static-files/                                                                                             

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR,'static')

終わったら、djangoプロジェクトの静的ファイルをまとめたり、
管理者ページに入るまでの設定をしておく。

django
$ cd ~/mysite
# 自身の情報をここで入れておく。パスは忘れないように
$ python3 manage.py createsuperuser
# STATICファイルをとりまとめておく
$ python3 manage.py collectstatic

もし、python3 manage.py collectstaticでcheck_sqlite_versionと書かれたエラーを吐いた場合はSQLite3の設定を見直すこと。過去のSQLite3を見に行っている可能性が高い。

Djangoそのものの設定がうまくできているか確認するために、gunicornから動かしてみる。

django
# 必ずmanage.pyなどが存在しているフォルダへ移動すること
$ cd ~/mysite
$ sudo gunicorn mysite.wsgi --bind=0.0.0.0:8000

ブラウザでパブリックIPあるいはドメインへアクセスした際に、例のロケットが表示されればOK。確認できたらgunicornをCtrl+Xなどで終了する。

gunicornの設定

以下の設定ファイルを作成(編集)する。

  • /etc/systemd/system/gunicorn.service
  • /etc/systemd/system/gunicorn.socket

なお、gunicorn.serviceのExecStartのパスはwhichコマンドで確認する。

gunicorn.serviceのExecStartに設定するパスのチェック
which gunicorn

ユーザー名(グループ名)、WorkingDirectory、ExecStartのwsgiファイル名とListenStreamは各自の環境に揃えておくこと。(特に工夫がなければ、ユーザー名はnginxと一致させておく。)

gunicorn.service
[Unit]
Description=gunicorn daemon
Requires=gunicorn.socket
After=network.target

[Service]
Type=notify
# the specific user that our service will run as
# ここのユーザ名とグループ名はデーモンを実行するユーザを入れる。
# 特に何も考えていなければec2-userにする。
User=ec2-user
Group=ec2-user
# another option for an even more restricted service is
# DynamicUser=yes
# see http://0pointer.net/blog/dynamic-users-with-systemd.html
RuntimeDirectory=gunicorn
# Djangoプロジェクトのパスを入れる
WorkingDirectory=/home/ec2-user/mysite
# gunicornの実行ファイルのあるパスと、プロジェクト名.wsgi
ExecStart=/usr/local/bin/gunicorn mysite.wsgi
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true

[Install]
WantedBy=multi-user.target
gunicorn.socket
[Unit]
Description=gunicorn socket

[Socket]
# sockファイルを作る場所を入れる。基本はdjangoプロジェクトのパスを入れておく。
ListenStream=/home/ec2-user/mysite/mysite.sock
# Our service won't need permissions for the socket, since it
# inherits the file descriptor by socket activation
# only the nginx daemon will need access to the socket
# gunicorn.serviceと同じユーザを入れる
SocketUser=ec2-user
# Optionally restrict the socket permissions even more.
# SocketMode=600

[Install]
WantedBy=sockets.target

ここで自分が間違えたポイントとして、sockファイルとsocketファイルを混同させないようにすること。sockファイルはnginxとgunicornがやり取りする際に使うファイルである。

終わったら、gunicornのデーモンの起動と自動起動の登録をする。

gunicorn起動と自動起動設定
$ sudo systemctl start gunicorn.service
$ sudo systemctl enable gunicorn
# ちゃんとプロセスが動いていることを確認する。
$ sudo systemctl status gunicorn

プロセスが正常に起動していれば、次に進む。

nginxの設定

まず、nginxが正常に動いていて、ネットからつながることを確認する。つながらない場合は、EC2ダッシュボードのインスタンス→セキュリティからポート80が開放されているか、あるいはEC2のダッシュボード上で示されているパブリックIPアドレスやドメインが正しいか確認する。

nginx
systemctl start nginx
# コマンド実行後にEC2のパブリックIPアドレスにブラウザからアクセスして、
# nginxが動いているページが見えることを確認する。
# 確認できたら、以下のコマンドで一旦終了する。
systemctl stop nginx

確認できたら、nginxの設定をする。設定するファイルは次の通り。(mysite.confは新規作成する。また、拡張子が.confなら好きな名前でOK)

  • /etc/nginx/nginx.conf
  • /etc/nginx/conf.d/mysite.conf
nginx.conf
# 全文は多すぎるので省略。userをnginxからec2-userなどのサービスを動かすユーザーへ変える。
# 思い入れがなければ、gunicorn起動時のユーザと一致させておく
...
user ec2-user;
...
# httpブロック内などに、この一行があるか確認する。書いてあればOK
include /etc/nginx/conf.d/*.conf;
...
mysite.conf
server {
    listen  80;
    # ドメインあるいはIPアドレスを設定する
    server_name     *.*.*.*;

    # djangoの静的ファイルの置き場所を指定する。
    location /static {
        alias /home/ec2-user/mysite/static;
    }

    # djangoのadmin用の静的ファイルの置き場所を指定する。
    location /static/admin {
        alias /home/ec2-user/mysite/static/admin;
    }

    # gunicorn起動時に作成されるsockファイルの場所を指定する。
    location / {
        proxy_pass http://unix:/home/ec2-user/mysite/mysite.sock;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

}

作成したら、デーモンをstart(あるいはrestart)させる。statusでプロセスが問題なく起動していれば次に進む。

nginx
sudo systemctl start nginx
# すでに起動していた場合はsudo systemctl restart nginx
# プロセスが適切に動いていることを確認する
sudo systemctl status nginx

ブラウザから管理者画面を確認してみる

以下コマンドでnginxとgunicornのプロセスがバックグラウンドで動いていることを確認する。動いていなければエラーから各設定を修正してstartしておく。

プロセスのチェック
$ sudo systemctl status nginx
$ sudo systemctl status gunicorn

そのあと、自身のDjangoプロジェクトフォルダ(/home/ec2-user/mysite)内にmysite.sockが作成されていることを確認し、ブラウザで「自身のパブリックIPアドレスorドメイン/admin」で管理者画面を開いて見る。CSSを含めてAdminのログインページが表示されれば完了。

もしCSSが死んでいる場合はDjangoの設定でSTATIC_ROOTを忘れていたり、mysite.confのLocationやAliasを疑うといいかも。

また、もし502エラー(Bad Gateway)が出てくるようであれば、nginx.confやgunicorn.serviceのユーザーが同じではなかった(意図した権限を持ったユーザじゃなかった)という点や、同ファイルにおいてsockファイルへのパスが異なっている可能性を疑ってみてもいいかも。

今日はここまで。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

何も考えずにEC2にDjango×SQLite3×nginx×gunicornな環境を構築する

はじめに

AWSのEC2上でDjangoプロジェクトを展開しようとして丸一日悩んでしまったので、手順をまとめました。あくまでも自己責任でお願いします。

やったこと

EC2インスタンスを立てて何も考えずにDjangoを動かして、ネット上からアクセスできるようにする。
今回はSQLite3,nginx,gunicornを使います。

注意

  • ec2-userをそのまま使っていたり、セキュリティ的に怪しい所があるため注意すること。今回はAWSでdjangoプロジェクトをとりあえず動かせるようにすることを目的とする。
  • VPC、サブネット、インターネットゲートウェイの設定はすでに完了していて、EC2自体はネットから参照できる状態になっている所からスタートする
  • ssh(22)、HTTP(80)とDjango用(8000)のポートが開放されている状態を前提とする
  • DBはSQLite3を使用する。また、SQLite3のバージョンを最新にするため既存のソフトウェアでSQLite3を使用している際は注意すること。
  • Djangoの管理者画面がスタイルシートなどを含めて読み込めたところで本記事のゴールとする

参考にした文献

必要なものを揃える

本記事ではvenvなどは使わず、そのままpython3を使っていくため注意すること。

install
# 後述処理でパスワードを求められる可能性があるため設定する
$ sudo passwd ec2-user
$ sudo yum update
$ sudo yum install python3
# Amazon Linux 2で$sudo yum install nginx と入力するとコマンドを催促されるので、それを入力する。
$ sudo amazon-linux-extras install nginx1
$ sudo pip3 install django
$ sudo pip3 install gunicorn

Amazon Linux 2にもデフォルトでSQLite3がインストールされていたが、Djangoで求めるバージョン(3.8.3以降)にマッチしないため、SQLiteのアプデを実施する。

SQLite3のinstall
# 後述のSQLite3インストール時のmakeファイルのビルドに使用する。
$ sudo yum install gcc
# 最新版のSQLite3をインストールする。執筆当時は3.33.0が最新だった。(https://www.sqlite.org/download.html)
# 展開した後にmakeファイルをビルドしてmake installまで実施する
$ wget https://www.sqlite.org/2020/sqlite-autoconf-3330000.tar.gz
$ tar zxvf sqlite-autoconf-3330000.tar.gz
$ cd sqlite-autoconf-3330000
# makeファイルのビルド
$ ./configure --prefix=/usr/local
$ sudo make
$ sudo make install

次に、過去に入っていたSQLite3から現行のSQLite3を使用するように設定を通す。

SQLite3のinstallつづき
# すでにインストール済みのsqlite3を確認する
$ sudo find / -name sqlite3
# もともとあったやつは多分これ。バージョンが古いことを一応確認しておく。
/usr/bin/sqlite3 --version
# 今回インストールしたのは多分これ。バージョンが古いことを一応確認しておく。
/usr/local/bin/sqlite3 --version
# 古い方のsqlite3のフォルダ名を変え、また新しい方のsqlite3へパスを設定しておく
sudo mv /usr/bin/sqlite3 /usr/bin/sqlite3_old
sudo ln -s /usr/local/bin/sqlite3 /usr/bin/sqlite3
# ライブラリへのパスを通しておく
sudo touch ld.so.conf/sqlite3.conf
sudo echo /usr/local/lib >> sqlite3.conf
# 過去のsqlite3へのリンクとディレクトリを削除
$ sudo rm /lib64/libsqlite3.so.0
$ sudo rm -rf /lib64/libsqlite3.so.[過去のバージョン]
# ライブラリのパスを更新する
sudo ldconfig
# 新しいライブラリへのパス(/usr/local/lib)が通っているか確認する
$ ldconfig -p | grep sqlite
# 一応バージョンも確認する
sqlite3 --version

最後のバージョン確認で現行のSQlite3のものが出力されれば、次に進む。

Djangoの設定

とりあえずプロジェクトを作る。プロジェクト名は公式ドキュメントでおなじみのmysiteにする。

django-admin
# /home/ec2-userなどの任意のユーザのhomeディレクトリへ移動
$ cd /~
$ django-admin startproject mysite

今回はadmin画面が見えればOKなので、最低限の設定だけをする。

django-admin
$ cd mysite/mysite
$ vi settings.py

setting.pyにおいて設定するのは

  • 末尾にSTATIC_ROOTを追加(あわせてimport osも追記)
  • ALLOWED_HOSTSに自分のパブリックドメインやパブリックIPアドレスを設定

ぐらい。一応全文を載せておく

setting.py
import os
from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.                                                                                        
BASE_DIR = Path(__file__).resolve().parent.parent

# SECURITY WARNING: keep the secret key used in production secret!                                                                                      
SECRET_KEY = 'hogepugefugahogepugefuga...'

# SECURITY WARNING: don't run with debug turned on in production!                                                                                       
DEBUG = True

# 自分のドメインやパブリックIPアドレスとか
ALLOWED_HOSTS = ['*.*.*.*',]


# Application definition                                                                                                                                

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'mysite.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'mysite.wsgi.application'

# Database                                                                                                                                              
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases                                                                                         

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}


# Password validation                                                                                                                                   
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators                                                                          

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization                                                                                                                                  
# https://docs.djangoproject.com/en/3.1/topics/i18n/                                                                                                    

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)                                                                                                                
# https://docs.djangoproject.com/en/3.1/howto/static-files/                                                                                             

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR,'static')

終わったら、djangoプロジェクトの静的ファイルをまとめたり、
管理者ページに入るまでの設定をしておく。

django
$ cd ~/mysite
# 自身の情報をここで入れておく。パスは忘れないように
$ python3 manage.py createsuperuser
# STATICファイルをとりまとめておく
$ python3 manage.py collectstatic

もし、python3 manage.py collectstaticでcheck_sqlite_versionと書かれたエラーを吐いた場合はSQLite3の設定を見直すこと。過去のSQLite3を見に行っている可能性が高い。

Djangoそのものの設定がうまくできているか確認するために、gunicornから動かしてみる。

django
# 必ずmanage.pyなどが存在しているフォルダへ移動すること
$ cd ~/mysite
$ sudo gunicorn mysite.wsgi --bind=0.0.0.0:8000

ブラウザでパブリックIPあるいはドメインへアクセスした際に、例のロケットが表示されればOK。確認できたらgunicornをCtrl+Xなどで終了する。

gunicornの設定

以下の設定ファイルを作成(編集)する。

  • /etc/systemd/system/gunicorn.service
  • /etc/systemd/system/gunicorn.socket

なお、gunicorn.serviceのExecStartのパスはwhichコマンドで確認する。

gunicorn.serviceのExecStartに設定するパスのチェック
which gunicorn

ユーザー名(グループ名)、WorkingDirectory、ExecStartのwsgiファイル名とListenStreamは各自の環境に揃えておくこと。(特に工夫がなければ、ユーザー名はnginxと一致させておく。)

gunicorn.service
[Unit]
Description=gunicorn daemon
Requires=gunicorn.socket
After=network.target

[Service]
Type=notify
# the specific user that our service will run as
# ここのユーザ名とグループ名はデーモンを実行するユーザを入れる。
# 特に何も考えていなければec2-userにする。
User=ec2-user
Group=ec2-user
# another option for an even more restricted service is
# DynamicUser=yes
# see http://0pointer.net/blog/dynamic-users-with-systemd.html
RuntimeDirectory=gunicorn
# Djangoプロジェクトのパスを入れる
WorkingDirectory=/home/ec2-user/mysite
# gunicornの実行ファイルのあるパスと、プロジェクト名.wsgi
ExecStart=/usr/local/bin/gunicorn mysite.wsgi
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true

[Install]
WantedBy=multi-user.target
gunicorn.socket
[Unit]
Description=gunicorn socket

[Socket]
# sockファイルを作る場所を入れる。基本はdjangoプロジェクトのパスを入れておく。
ListenStream=/home/ec2-user/mysite/mysite.sock
# Our service won't need permissions for the socket, since it
# inherits the file descriptor by socket activation
# only the nginx daemon will need access to the socket
# gunicorn.serviceと同じユーザを入れる
SocketUser=ec2-user
# Optionally restrict the socket permissions even more.
# SocketMode=600

[Install]
WantedBy=sockets.target

ここで自分が間違えたポイントとして、sockファイルとsocketファイルを混同させないようにすること。sockファイルはnginxとgunicornが共有するファイルである。

終わったら、gunicornのデーモンの起動と自動起動の登録をする。

gunicorn起動と自動起動設定
$ sudo systemctl start gunicorn.service
$ sudo systemctl enable gunicorn
# ちゃんとプロセスが動いていることを確認する。
$ sudo systemctl status gunicorn

プロセスが正常に起動していれば、次に進む。

nginxの設定

まず、nginxが正常に動いていて、ネットからつながることを確認する。つながらない場合は、EC2ダッシュボードのインスタンス→セキュリティからポート80が開放されているか、あるいはEC2のダッシュボード上で示されているパブリックIPアドレスやドメインが正しいか確認する。

nginx
systemctl start nginx
# コマンド実行後にEC2のパブリックIPアドレスにブラウザからアクセスして、
# nginxが動いているページが見えることを確認する。
# 確認できたら、以下のコマンドで一旦終了する。
systemctl stop nginx

確認できたら、nginxの設定をする。設定するファイルは次の通り。(mysite.confは新規作成する。また、拡張子が.confなら好きな名前でOK)

  • /etc/nginx/nginx.conf
  • /etc/nginx/conf.d/mysite.conf
nginx.conf
# 全文は多すぎるので省略。userをnginxからec2-userなどのサービスを動かすユーザーへ変える。
# 思い入れがなければ、gunicorn起動時のユーザと一致させておく
...
user ec2-user;
...
# httpブロック内などに、この一行があるか確認する。書いてあればOK
include /etc/nginx/conf.d/*.conf;
...
mysite.conf
server {
    listen  80;
    # ドメインあるいはIPアドレスを設定する
    server_name     *.*.*.*;

    # djangoの静的ファイルの置き場所を指定する。
    location /static {
        alias /home/ec2-user/mysite/static;
    }

    # djangoのadmin用の静的ファイルの置き場所を指定する。
    location /static/admin {
        alias /home/ec2-user/mysite/static/admin;
    }

    # gunicorn起動時に作成されるsockファイルの場所を指定する。
    location / {
        proxy_pass http://unix:/home/ec2-user/mysite/mysite.sock;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

}

作成したら、デーモンをstart(あるいはrestart)させる。statusでプロセスが問題なく起動していれば次に進む。

nginx
sudo systemctl start nginx
# すでに起動していた場合はsudo systemctl restart nginx
# プロセスが適切に動いていることを確認する
sudo systemctl status nginx

ブラウザから管理者画面を確認してみる

以下コマンドでnginxとgunicornのプロセスがバックグラウンドで動いていることを確認する。動いていなければエラーから各設定を修正してstartしておく。

プロセスのチェック
$ sudo systemctl status nginx
$ sudo systemctl status gunicorn

そのあと、自身のDjangoプロジェクトフォルダ(/home/ec2-user/mysite)内にmysite.sockが作成されていることを確認し、ブラウザで「自身のパブリックIPアドレスorドメイン/admin」で管理者画面を開いて見る。CSSを含めてAdminのログインページが表示されれば完了。

もしCSSが死んでいる場合はこのあたりを疑ってみる。

また、もし502エラー(Bad Gateway)が出てくるようであれば、このあたりを間違えているかも。

  • nginx.confやgunicorn.serviceのユーザーが同じではなかった(意図した権限を持ったユーザじゃなかった)
  • 同ファイルにおいてsockファイルへのパスが異なっている。

今日はここまで。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ドメイン取得とSSL証明書を導入してWEBサーバを公開

はじめに

これからインフラエンジニアを目指す人や契約したサーバを公開したい人にとって
役にたつ資料になればと思い投稿しました。使うものは以下です。

・Server:固定GlobalIPアドレス、Apacheインストールを事前に行ってください。
・Freenom:無料でドメインを取得できるサービスを提供。1年間であれば無料でドメインを利用できます。
・Let's Encrypt:90日間有効な無償SSL/TLSサーバー証明書を発行できます。

FreenomeにてDomainの取得

1) Freenomにアクセスし、アカウントを作成します。
2) アカウントを作成したら、Services → 登録をクリックします。
3) 新しい無料ドメインを探しますで、取得したいドメイン名を入れます。
4) 取得可能ドメイン一覧が表示されますが無料のやつから「今すぐ入手!」をクリックします。
5) カートに入ったらチェックアウトをクリックします。
6) 登録時に以下画面が表示されます。
・Use DNSを選択します。
・IP Address欄にはサーバのグローバルIPアドレスを入力します。
・Periodには12Months @ Freeをプルダウンより選択します。
WS000000.JPG
7) Continueをクリックします。
8) I have read and agree to the Terms & Conditionsにチェックを入れて、Complete Orderをクリックします。
9) 上部メニューのMy Domainsより、取得したドメインがあることを確認できれば完了です。

SSL証明書の導入

サーバにログインし、必要なものをインストールします。

# yum install epel-release
# yum install certbot python-certbot-apache

次にSSL証明書の導入のためにSSL証明書を作成します。です。
DocumentRootにはApacheで設定でしているDocumentRootを、Domain名には取得したドメイン名を記載して下さい。

# certbot certonly --webroot -w [DocumentRoot] -d [Domain名]

有効なメールアドレスを入力します。

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Enter email address (used for urgent renewal and security notices) (Enter 'c' to
cancel): メールアドレスを入力

規約に対してAgreeします。

Starting new HTTPS connection (1): acme-v01.api.letsencrypt.org
-------------------------------------------------------------------------------
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf. You must agree
in order to register with the ACME server at
https://acme-v01.api.letsencrypt.org/directory
-------------------------------------------------------------------------------
(A)gree/(C)ancel: A

証明書に関することのメールを受け取りたい場合にはYを、そうではなければNを入力します。

-------------------------------------------------------------------------------
Would you be willing to share your email address with the Electronic Frontier
Foundation, a founding partner of the Let's Encrypt project and the non-profit
organization that develops Certbot? We'd like to send you email about EFF and
our work to encrypt the web, protect its users and defend digital rights.
-------------------------------------------------------------------------------
(Y)es/(N)o: N

正しく証明書の作成が行われた場合は、次のように出力されます。

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/[Domain名]/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/[Domain名]/privkey.pem
   Your cert will expire on 2020-12-27. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

作成された証明書のファイルを確認します。

# ls /etc/letsencrypt/live/[Domain名]/
cert.pem  chain.pem  fullchain.pem  privkey.pem  README

Apacheのssl.confに証明書のパスを記載します。

# cat /etc/httpd/conf.d/ssl.conf
SSLCertificateFile /etc/letsencrypt/live/[Domain名]/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/live/[Domain名]/privkey.pem
SSLCertificateChainFile /etc/letsencrypt/live/[Domain名]/chain.pem

Apacheを再起動します。

# systemctl restart httpd

サイトアクセスしSSL証明書が導入されたことを確認

取得したドメイン名でChromeやFireFoxでhttpsアクセスを行いURLバーの鍵マークをクリックし、
証明書が反映できていることを確認します。
WS000000.JPG

おまけ SSL証明書自動更新設定

おまけですが、Let's Encryptには証明書を自動更新する機能があるので、
それをCRONで実行することで SSL証明書自動更新をすることができます。

# crontab -u root -e
00 02 01 * * certbot renew && systemctl restart httpd
※毎月1日の2時に更新チェックを実施

最後に

これからインフラエンジニアを目指す人や作ったWEBサーバを公開したい人向けに、
すごく昔やったことを書いてみました。
単なる自分の興味だけでなく、基礎的な内容についても時間があるときに書いてみたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RDS Proxy+RDS(MySQL)の接続について

概要

RDSとRDS Proxyの接続の手順やエラーの解消についてメモです。

RDS Proxy(MySQL)について

制約

使えるリージョン

  • 米国東部 (バージニア北部)
  • 米国東部 (オハイオ)
  • 米国西部 (北カリフォルニア)
  • 米国西部 (オレゴン)
  • アジアパシフィック (ムンバイ)
  • アジアパシフィック (ソウル)
  • アジアパシフィック (シンガポール)
  • アジアパシフィック (シドニー)
  • アジアパシフィック (東京)
  • カナダ (中部)
  • 欧州 (フランクフルト)
  • 欧州 (アイルランド)
  • 欧州 (ロンドン)

対象RDSのMySQLバージョン

MySQL 5.6/5.7
MySQL 8.0では使用不可です

アクセス

接続対象のデータベースと同一VPCに設置する必要があります。
プロキシはパブリックアクセス出来ないため、Lambdaと接続する場合はLambdaも同じVPCに設置する。

ピン留め

RDSProxyでは、接続が特定のクライアントに固定され、セッション終了まで他のクラアントからの接続が行えなくなる状態に陥ることがあり、
これが発生すると接続をうまいことさばいてくれるRDS proxyの意味がなくなってしまいます。

発生する条件

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/rds-proxy.html#rds-proxy-pinning

RDS Proxy は、文字セット、照合順序、タイムゾーン、自動コミット、現在のデータベース、SQL モード、および session_track_schema の各設定への変更を追跡します。したがって、これらの変更時に RDS Proxy はセッションをピン留めしません。この場合、RDS Proxy は、これらの設定の値が同じである他のセッションでのみ、接続を再利用します

  • SETSELECTコマンドでの変数の変更
  • LOCK TABLELOCK TABLESコマンド
  • 一時テーブルの作成

等がピン留めの原因になります。

ピン留めを回避するパラメータについては別にまとめたいと思います。

手順

大まかな流れ

VPC・RDS作成する

省略します。
RDSはRDS Proxyの制約に合ったものを作ります。

セキュリティグループを作成してRDSに割り当てておきます。(sg-rds)

AWS Secrets Manager でデータベース認証情報を作成

ユーザー名とパスワードの認証情報をSecrets Managerに保存する

usernameとpasswordのフィールドに接続したいRDSの情報と一致するように登録する

AWS Secrets Manager > シークレット > 新しいシークレットを保存する

AWS CLIで作成する場合

$ aws secretsmanager create-secret
--name "secret_name"
--description "secret_description"
--region region_name
--secret-string '{"username":"db_user","password":"db_user_password"}'

IAMロールの作成

新規作成

RDS > Add Role to Database

ポリシーの作成

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "secretsmanager:GetSecretValue",
            "Resource": [
            // 複数ある場合は複数
                "作成したシークレットのARN",
            ]
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "kms:Decrypt",
            "Resource": "KMSのARN",
            "Condition": {
                "StringEquals": {
                    "kms:ViaService": "secretsmanager.us-east-1.amazonaws.com"
                }
            }
        }
    ]
}

KMSのARNは特に設定していない場合、
Key Management Service (KMS) > AWS マネージド型キー > aws/secretsmanager
を使用します。

RDS Proxyを作成

コンソールより作成

スクリーンショット 2020-09-28 19.04.28.png

プロキシ識別子 > RDS Proxyに名前を付けます
エンジンの互換性 > 今回はMySQL

スクリーンショット 2020-09-28 19.04.42.png

データベース > 作成した対象のRDSを選択します
接続プールの最大接続数 > とりあえずデフォルトのままに

スクリーンショット 2020-09-28 19.29.29.png

Secrets Manager シークレット > 先ほど作成したシークレットを設定
IAM ロール > 先ほど作成したIAMロールを設定
IAM認証 > 今回は無効にします
サブネット > RDSに割り当てたサブネットグループと同じ物を選択します

スクリーンショット 2020-09-28 19.19.23.png

VPCセキュリティグループ > 新規作成。分かりやすい名前を付けます(sg-rdsproxy)

「プロキシを作成」

セキュリティグループ

EC2 > セキュリティグループ > [セキュリティグループ] > インバウンドルールを編集
RDSのセキュリティグループ(sg-rds)の設定で、RDS Proxyのセキュリティグループ(sg-rdsproxy)からのインバウンドを許可します。

MYSQL/Aurora TCP 3306 sg-rdsproxy

ステータスが「利用可能」になったら完了です。

「接続不可」になるとき

このコマンドで接続不可の理由をチェック出来きます。
参考:https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/rds-proxy.html#rds-proxy-verifying

$aws rds describe-db-proxy-targets --db-proxy-name

エラー例1

"TargetHealth": {
    "State": "UNAVAILABLE",
    "Reason": "PENDING_PROXY_CAPACITY",
    "Description": "DBProxy Target unavailable due to an internal error"
}

プロキシがスケーリングオペレーションを完了した後で、接続を再試行します

20分ぐらい待ってから確認してみます。

エラー例2

"TargetHealth": {
    "State": "UNAVAILABLE",
    "Description": "DBProxy Target unavailable due to an internal error"
}

セキュリティグループの設定に問題がないか確認してみる。

エラー例3

"TargetHealth": {
    "State": "UNAVAILABLE",
    "Reason": "AUTH_FAILURE",
    "Description": "Proxy does not have any registered credentials"
}

IAMロールに問題が無いか確認。

エラー例4

"TargetHealth": {
    "State": "UNAVAILABLE",
    "Reason": "AUTH_FAILURE",
    "Description": "Database authentication failed with provided user credentials"
}

シークレットの設定に問題が無いか確認。

OK

"TargetHealth": {
    "State": "AVAILABLE"
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS Fargateで動かしてるコンテナの中に入る方法

はじめに

AWS Fargateの場合、トラブルがあった時に動かしてるコンテナの中に入ってなくて調査できないのが不便なのでコンテナに入る方法を調査した。

  • 現時点(2020年9月)でAWS Fargateの独自機能でコンテナの中に入る方法はない
  • コンテナにsshdをインストールする方法とssm-agentをインストールする方法がある

ssm-agentを使うとSSHのポートの開放やSSHする公開鍵の管理などをしなくて済むためssm-agentを使う方法で行った。

コンテナにsshdをインストールする方法

  1. SSHのポートを空けておく
  2. コンテナの中の~/.ssh/authorzied_keysにsshするユーザーの公開鍵を追加しておく
  3. コンテナの中にsshdをインストールしておいて、コンテナ起動時にsshdを立ち上げておく

メリット

  • セッションマネージャーを使わないため、セッションマネージャーを使う料金はかからない

デメリット 

  • SSHのポートを開放する必要がある
  • SSHするユーザーの公開鍵を管理する必要がある

コンテナにssm-agentをインストールする方法

  1. コンテナ起動時にハイブリッドアクティベーションでアクティベーションを作成して登録してssm-agentを起動
  2. セッションマネージャーを使ってコンテナの中に入る

メリット

セッションマネージャーを使うので以下のメリットがある。

  • SSHのポートを開放する必要がなくなる
  • SSHするユーザーの公開鍵を管理する必要がなくなる 

デメリット

  • 通常のAmazon EC2に対してセッションマネージャーを使う料金はかからないが、ハイブリッドアクティベーションを使って登録することから、オンプレミスインスタンス管理の扱いになるため利用料金がかかる

https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/activations.html

Amazon EC2 インスタンスへのアクセスは、追加料金なしでご利用いただけます。

ソースコード

https://github.com/f96q/fargate-ssm-sample

DockerfileとAWS Fargateで動かす環境を作るためのTerraform含む

Dockerfile

Alpineで使う場合はssm-agentのパッケージがないのでソースから持ってきてビルドして設置する必要がある。

他のLinuxディストーションの場合はインストールできるssm-agentのパッケージを提供してる場合があるので、その場合はそのパッケージをインストールするだけで済む。
https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/sysman-manual-agent-install.html

ARG GOLANG_TAG=1.14.4-alpine3.12
ARG ALPINE_TAG=3.12

# ssm agenet builder
FROM golang:$GOLANG_TAG as ssm-agent-builder

ARG SSM_AGENT_VERSION=2.3.1205.0

RUN apk add --no-cache \
         'make~=4.3-r0' \
         'git~=2.26.2-r0' \
         'gcc~=9.3.0-r2' \
         'libc-dev~=0.7.2-r3' \
         'bash~=5.0.17-r0'

RUN wget -q https://github.com/aws/amazon-ssm-agent/archive/${SSM_AGENT_VERSION}.tar.gz && \
    mkdir -p /go/src/github.com && \
    tar xzf ${SSM_AGENT_VERSION}.tar.gz && \
    mv amazon-ssm-agent-${SSM_AGENT_VERSION} /go/src/github.com/amazon-ssm-agent && \
    echo ${SSM_AGENT_VERSION} > /go/src/github.com/amazon-ssm-agent/VERSION

WORKDIR /go/src/github.com/amazon-ssm-agent

RUN gofmt -w agent && make checkstyle || ./Tools/bin/goimports -w agent && \
    make build-linux

# merge image
FROM alpine:$ALPINE_TAG

RUN apk add --no-cache \
      'jq~=1' \
      'aws-cli~=1.18.55-r0' \
      'sudo~=1.9.0-r0'

RUN adduser -D ssm-user && \
    echo "Set disable_coredump false" >> /etc/sudo.conf && \
    echo "ssm-user ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/ssm-agent-users && \
    mkdir -p /etc/amazon/ssm

COPY --from=ssm-agent-builder /go/src/github.com/amazon-ssm-agent/bin/linux_amd64/ /usr/bin
COPY --from=ssm-agent-builder /go/src/github.com/amazon-ssm-agent/bin/amazon-ssm-agent.json.template /etc/amazon/ssm/amazon-ssm-agent.json
COPY --from=ssm-agent-builder /go/src/github.com/amazon-ssm-agent/bin/seelog_unix.xml /etc/amazon/ssm/seelog.xml

COPY docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]

CMD ["amazon-ssm-agent"]
docker-entrypoint.sh
#!/bin/sh

set -e


AWS_REGION=${AWS_REGION:-}
SSM_ACTIVATION=$(aws ssm create-activation --default-instance-name "fargate-ssm" --iam-role "service-role/AmazonEC2RunCommandRoleForManagedInstances" --registration-limit 1 --region $AWS_REGION)

export SSM_ACTIVATION_CODE=$(echo $SSM_ACTIVATION | jq -r .ActivationCode)
export SSM_ACTIVATION_ID=$(echo $SSM_ACTIVATION | jq -r .ActivationId)

amazon-ssm-agent -register -code $SSM_ACTIVATION_CODE -id $SSM_ACTIVATION_ID -region $AWS_REGION

exec "$@"

使い終わったら手動で行わないといけないこと

https://aws.amazon.com/jp/systems-manager/pricing/#On-Premises_Instance_Management

動かしてる時間課金されてしまうため以下を行う。

  • ハイブリッドアクティベーションの削除
  • 登録したインスタンスを外す
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CloudWatchEvents (EventBridge) + StepFunctionsを使ったCron処理で同時起動を防ぐ方法

コンテナ化やサーバレス化が進み、従来実装していた方法が通用しない場面が出てきています。
その代表的なものの一つがバッチ処理です。
従来はバッチ処理を行うサーバが存在し、crontabコマンドで設定などを行っていました。
しかしコンテナ化、サーバレス化の影響でバッチサーバというものは存在しなくなります。

AWS上でサーバレスなバッチ処理システムを構築するための方法の一つにCloudWatchEvents(最近、EventBridgeになりました。以下CWEと略します)を利用する方法があります。
しかし、このCWEにはイベントが多重に発生するという仕様があります。なお、Kubernetsの CronJob やGCPの Cloud Scheduler でも同様の仕様になっています。
バッチに同実行耐性や冪等性を持たせておけば問題ないのですが、長時間バッチや特殊処理を行うバッチ、作られたのが古くて手を入れづらいバッチなどはなかなか簡単にはいきません。

概要

今回は、CWE + StepFuntionsを使ったバッチ処理で同時実行を防止する方法を考えました。
バッチ処理自体はStepFuntionsからLambdaかECS Taskを呼び出して実行することを想定しています。
アーキテクチャとしては、LambdaとDynamoDBを使ったものになります。

アーキテクチャ

CWE+SF排他制御アーキテクチャ.png

DynamoDBを利用する理由

排他制御を行うのに重要なのは、ReadとWriteをアトミックに行えることです。
Readとはロックされているかどうか確認する処理であり、Writeとはロックする処理です。

ぱっと思いつく利用できそうなやり方としては下記があります。

  • Elasticache(Redis)を利用する
  • RDS(Mysql)を利用する

RedisではReadとWriteをそのままではアトミックに処理できません。
luaスクリプト書けば出来ますが、ちょっと面倒です。

Mysqlは普通にトランザクション利用すればできますが、この処理のためだけに利用するにはちょっと重いです。

そこでDynamoDBを利用します。
DynamoDBはOptimisticLockを利用できるようになっており、サーバレスでスキーマレスなDBであり、利用も料金もかなりお手軽です。

StepFunctionsのステートグラフ

こんな感じになると思います。
アンロックはバッチ処理本体の成功失敗に関わらず必ず実行されるようにします。

{
    "Comment": "CWE+StepFunctionsを利用したバッチ処理の排他制御ステートグラフ",
    "StartAt": "Lock",
    "States": {
        "Lock": {
            "Type": "Task",
            "Resource": "ロック関数のARN",
            "Parameters": {
                "Key": "hogekey",
                "Ttl": 300
            },
            "Catch": [
                {
                    "ErrorEquals": ["LockError"],
                    "Next": "Lock Error"
                },
                {
                    "ErrorEquals": ["States.ALL"],
                    "Next": "Other Lock Error"
                }
            ],
            "ResultPath": "$.Lock",
            "Next": "Lock Ok"
        },
        "Lock Ok": {
            "Type": "Pass",
            "Next": "Run"
        },
        "Other Lock Error": {
            "Type": "Pass",
            "Next": "Lock Fail"
        },
        "Lock Error": {
            "Type": "Pass",
            "Next": "Lock Fail"
        },
        "Lock Fail": {
            "Type": "Fail",
            "Cause": "Can not get lock."
        },
        "Run": {
            "Type": "Task",
            "Resource": "バッチ処理のARN",
            "Parameters": {
                "Command": "hoge"
            },
            "Catch": [
                {
                    "ErrorEquals": ["States.ALL"],
                    "Next": "Run Error"
                }
            ],
            "ResultPath": "$.Cron",
            "Next": "Run Ok"
        },
        "Run Ok": {
            "Type": "Pass",
            "Result": {
                "statusCode": 200
            },
            "ResultPath": "$.Run",
            "Next": "UnLock"
        },
        "Run Error": {
            "Type": "Pass",
            "Result": {
                "statusCode": 400
            },
            "ResultPath": "$.Run",
            "Next": "UnLock"
        },
        "UnLock": {
             "Type": "Task",
             "Resource": "アンロック関数のARN",
             "Parameters": {
                 "key": "hogekey"
             },
             "ResultPath": "$.UnLock",
             "Next": "Check Run Result"
        },
        "Check Run Result": {
            "Type": "Choice",
            "Choices": [
                {
                    "Not": {
                        "Variable": "$.Run.statusCode",
                        "NumericEquals": 200
                    },
                    "Next": "Run Fail"
                }
            ],
            "Default": "Run End"
        },
        "Run End": {
            "Type": "Pass",
            "End": true
        },
        "Run Fail": {
            "Type": "Fail",
            "Cause": "Run failed."
        }
    }
}

CWE+SF排他制御SFステートグラフ.png

DynamoDBスキーマ

{
  "Key": "hoge",
  "Ttl": 300
}
  • Key: 文字列
    • プライマリーキーです。実行するバッチ処理の名前とか入れておくと良いと思います
  • Ttl: 数値
    • DynamoDBのttl機能を利用するための項目です
    • アンロック処理が失敗した場合の保険です。最大48時間の遅延があります

ロック/アンロック関数

DynamoDBのPutでロックを、Deleteでアンロックを実現します。
実際にPut/Deleteする部分のみ抜粋します。
以下のlambdaコードはnode.jsです。

ロック

const DynamoDB = require('aws-sdk/clients/dynamodb');
const ddb = new DynamoDB.DocumentClient({
    region: "リージョンを指定",
});

function LockError(message) {
    this.name = 'LockError';
    this.message = message;
}
LockError.prototype = new Error();

const lock = async (key, ttl) => {
    const nowTime = Math.floor((new Date).getTime()/1000);
    const ttlTime = nowTime + ttl; // DynamoDBに設定するttlは絶対時間
    const params ={
        TableName: "LockTable",
        Item: {
            Key: key,
            Ttl: ttlTime,
        },
        ExpressionAttributeNames: {
            '#k': 'Key',
        },
        ConditionExpression: 'attribute_not_exists(#k)', // 今回の一番の肝になる部分。アトミックにロック確認とロック処理を行える
    };

    try {
        await ddb.put(params).promise();
    } catch (e) {
        const message = 'Lock Fail.';
        throw new LockError(message); // 独自ロックエラーオブジェクト
    }
};
// :
// 以下略
// :

アンロック

const DynamoDB = require('aws-sdk/clients/dynamodb');
const ddb = new DynamoDB.DocumentClient({
    region: "リージョンを指定",
});

function UnLockError(message) {
    this.name = 'UnLockError';
    this.message = message;
}
UnLockError.prototype = new Error();

const unlock = async key => {
    const params = {
        TableName: "LockTable",
        Key: {
            Key: key
        },
        ExpressionAttributeNames: {
            '#k': 'Key'
        },
        ConditionExpression: 'attribute_exists(#k)',
    };

    try {
        await ddb.delete(params).promise();
    } catch (e) {
        const message = 'UnLock Fail.';
        throw new UnLockError(message); // 独自アンロックエラーオブジェクト
    }
};
// :
// 以下略
// :

まとめ

DynamoDBを利用することで簡単に排他制御を実装できました。
しかもDynamoDBは運用が非常に簡単です。
KubernetesやGCPを利用しているとまた違った方法が必要になりますが、ECSやLambdaを運用している場合はこれでなんとかなりそうです。
ただし、連続実行は防げないので、新しく作るバッチ処理はクラウドネイティブを意識して冪等性などを考えながら作っていく必要があります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWSとPHPでTwitterOAuthでログイン機能を実装

TWitterOAuthでログイン機能実装が思ったより苦戦したので、エラーになったポイントを伝えていきます。
AWS上でやっていきます。

AWSでComposerをインストール

①下記手順を実施してPHPを入れる。(実施済みの方は飛ばす。)
AWS EC2 AmazonLinux2 PHPをインストールする

②公式のインストール方法に記載されているコマンドを実行してcomposer本体を取得する。
コマンドは変更になる可能性があるので最新のコマンドは公式ページをご確認してください。

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === '795f976fe0ebd8b75f26a6dd68f78fd3453ce79f32ecb33e7fd087d39bfeb978342fb73ac986cd4f54edd0dc902601dc') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"

③下記コマンドを実行してcomposerの実行ファイルを移動する。

$ sudo mv composer.phar /usr/local/bin/composer

④下記コマンドを実行してcomposerの実行ファイルを実行する。

$ php /usr/local/bin/composer

⑤下記コマンドを実行してパスを通す。(ユーザ名にはssh接続しているEC2インスタンスのユーザ名を入力する。Apacheのディレクトリルートで実行することを考えフルパスでパスを通す。)

$ echo "export PATH=/home/ec2-user/.config/composer/vendor/bin:$PATH" >> ~/.bash_profile
$ source ~/.bash_profile

Twitteroauthをインストール

⑥公式Twitteroauthサイトで書いてある通りに実行する。

composer require abraham/twitteroauth

⑦vendorのフォルダの中に実行ファイルのautoload.phpがある為、htmlフォルダに移動。

mv vendor /var/www/html/

Twitter OAuth認証のPHP実装

⑧動作環境
PHP Version 5.3以上であること(phpinfo();関数で確認)
cURL support enabledであること(phpinfo();関数で確認)
デフォルトで多分、大丈夫です。

⑨Callback URLは忘れずに
Twitterアプリケーションの「Settings」→「Application Details」→「Callback URLs」には、「callback.php」のURLを指定します。以下のサンプルコードでは、「'http://' . $_SERVER['HTTP_HOST'] . 'callback.php」が「Callback URLs」に指定されています。

⑩config.php

<?php
//アプリケーションの Consumer Key と Consumer Secret
$sTwitterConsumerKey = '***********************************'; //Consumer Key (API Key)
$sTwitterConsumerSecret = '***********************************'; //Consumer Secret (API Secret)

//アプリケーションのコールバックURL
$sTwitterCallBackUri = 'http://' . $_SERVER['HTTP_HOST'] . '/callback.php'; //コールバックURL

//変数初期化
$objTwitterConection = NULL; //TwitterOAuthクラスのインスタンス化
$aTwitterRequestToken = array(); //リクエストトークン
$sTwitterRequestUrl = ''; //認証用URL
$objTwitterAccessToken = NULL; //アクセストークン
$objTwUserInfo = NULL; //ユーザー情報
?>

⑪login.php

<?php
##############################################
### 初期設定
//エラー詳細
ini_set('display_errors', 1);

//セッションスタート
session_start();

//文字セット
header("Content-type: text/html; charset=utf-8");

//インクルード
require_once(__DIR__ . '/config.php');
require_once(__DIR__ . '/vendor/autoload.php');

//インポート
use Abraham\TwitterOAuth\TwitterOAuth;

##############################################
### twitter oauth request token 取得

//TwitterOAuthクラスをインスタンス化
$objTwitterConection = new TwitterOAuth($sTwitterConsumerKey, $sTwitterConsumerSecret);

//oauthリクエストトークンの取得
$aTwitterRequestToken = $objTwitterConection->oauth('oauth/request_token', array('oauth_callback' => $sTwitterCallBackUri));

//oauthリクエストトークンをセッションに格納
$_SESSION['twOauthToken'] = $aTwitterRequestToken['oauth_token'];
$_SESSION['twOauthTokenSecret'] = $aTwitterRequestToken['oauth_token_secret'];

##############################################
### twitter 認証へ

//Twitter認証URLの作成
$sTwitterRequestUrl = $objTwitterConection->url('oauth/authenticate', array('oauth_token' => $_SESSION['twOauthToken']));

//Twitter認証画面へリダイレクト
header('location: '.$sTwitterRequestUrl);
?>

⑫logout.php

<?php
##############################################
### 初期設定

//セッションスタート
session_start();

//文字セット
header("Content-type: text/html; charset=utf-8");

//セッション変数を全て解除
$_SESSION = array();
$_COOKIE = array();

//クッキー削除
setcookie("PHPSESSID", '', time() - 1800, '/');

//セッションを破棄する
session_destroy();
?>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>タイトル</title>
<meta http-equiv="Content-Style-Type" content="text/css">
</head>

<h2>Twitter アカウント ログアウト</h2>

<?php
echo "ログアウトしました。"; 
echo "<a href='https://wepicks.net/code-example/twitter-restapi/login/login.php'>ログインへ</a>";
?>

</body>
</html>

⑬member.php

<?php
##############################################
### 初期設定

//セッションスタート
session_start();

//文字セット
header("Content-type: text/html; charset=utf-8"); 

//インクルード
require_once(__DIR__ . '/config.php');
require_once(__DIR__ . '/vendor/autoload.php');

//インポート
use Abraham\TwitterOAuth\TwitterOAuth;

##############################################
### アクセストークン確認
if(empty($_SESSION['twAccessToken'])){
 echo 'error access token!!';
 exit;
}

##############################################
### ユーザー情報の取得

//TwitterOAuthクラスをインスタンス化
$objTwitterConection = new TwitterOAuth
 (
 $sTwitterConsumerKey,
 $sTwitterConsumerSecret,
 $_SESSION['twAccessToken']['oauth_token'],
 $_SESSION['twAccessToken']['oauth_token_secret']
 );

//ユーザー情報を取得
$objTwUserInfo = $objTwitterConection->get("account/verify_credentials");
?>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>タイトル</title>
<meta http-equiv="Content-Style-Type" content="text/css">
</head>

<h2>Twitter アカウント ログイン完了!</h2>

<?php echo $_SERVER['REQUEST_URI']; ?><br/>
<a href="logout.php">ログアウト</a>

<pre>
<?php var_dump($_SESSION['twAccessToken']); ?>
</pre>

<pre>
<?php var_dump($objTwUserInfo); ?>
</pre>

</body>
</html>

⑭callback.php

<?php
##############################################
### 初期設定

//セッションスタート
session_start();

//文字セット
header("Content-type: text/html; charset=utf-8"); 

//インクルード
require_once(__DIR__ . '/config.php');
require_once(__DIR__ . '/vendor/autoload.php');

//インポート
use Abraham\TwitterOAuth\TwitterOAuth;

##############################################
### oauthトークン確認
if(empty($_SESSION['twOauthToken']) || empty($_SESSION['twOauthTokenSecret']) || empty($_REQUEST['oauth_token']) || empty($_REQUEST['oauth_verifier'])){
 echo 'error token!!';
 exit;
}
if($_SESSION['twOauthToken'] !== $_REQUEST['oauth_token']) {
 echo 'error token incorrect!!';
 exit;
}

##############################################
### アクセストークン作成

//取得したoauthトークンでTwitterOAuthクラスをインスタンス化
$objTwitterConection = new TwitterOAuth
 (
 $sTwitterConsumerKey,
 $sTwitterConsumerSecret,
 $_SESSION['twOauthToken'],
 $_SESSION['twOauthTokenSecret']
 );

//アクセストークンの取得
$_SESSION['twAccessToken'] = $objTwitterConection->oauth("oauth/access_token", array("oauth_verifier" => $_REQUEST['oauth_verifier']));

//メンバーページへリダイレクト
header('location: member.php');

⑮確認
完成したのか、確認をします。
login.phpにアクセスしてみます。
こんなになってたら、完成です。
api.png

参照させていただきました。
PHP で Twitter API OAuth 認証 「ログイン」
twitteroauthのインストール方法
AWS EC2 AmazonLinux2 composerをインストールする

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[AWS]EC2,RDS,Djangoを使ってみた。1から環境構築

はじめに

今回初めてバックエンドを触ることになり、かなり手惑いました。
次回作成時この記事を見ればすぐに作成できるようにと思い、自分用または同じように手惑っている方の為に作成しました。

初心者ですので間違っている箇所や、考え方がおかしい場所があるかもしれません。その際は、コメント欄で教えていただけると幸いです。

$             <- 自分のPCターミナルでのコマンド
[ec2-user] $  <- EC2にログイン中でのコマンド
MySQL >       <- MySQLにログイン中でのコマンド
#             <- 私のコメント
>>>           <- 実行結果(出力値)

前提条件

  • AWSアカウント作成済

[1] AWS環境設定

[1.1] VPC作成

VPCとはユーザー専用のプライベートなクラウド環境を提供するサービスのことです。
例えば、EC2同士を内部的に通信したい場合や、RDSとやり取りをしたり、内部と外部のネットワークを繋いだりと、多くのAWSはVPCを利用しています。

作成手順

  • [1] VPCの作成
  • [2] 名前タグを「VPCtest」
  • [3] IPv4 CIDR ブロックを 「10.0.0.0/16」
  • [4] タグキーを「Name」 値を「VPCtest」 とする

スクリーンショット 2020-09-27 12.56.03.png
スクリーンショット 2020-09-27 12.59.42.png

これでVPCの作成が完了です。

[1.1.2]サブネット作成

サブネットは最初にVPCによって作られているCIDRブロックを分割したネットワーク群のことです。 サブネットは、VPCの上限を超えなければ、いくらでも作ることが可能です。 サブネットには、主にパブリックサブネットプライベートサブネットがあります。

最初はイメージがつきにくいと思うので...
広大な地球というインターネット空間自宅というVPCを作りました。
そこに風呂寝室など役割の違う部屋(サブネット)を作ったというイメージです。
まちがった考え方だったらごめんなさい(;´Д`)

パブリックサブネットとプライベートサブネットを作成します。
パブリックサブネットにはEC2
プライベートサブネットにはRDSを入れます。
RDSには異なるアベイラビリティゾーンを設定する必要がある為プライベートサブネットは2つ作成します。

作成手順

  • サブネットの作成
  • [1] パブリックtest作成
  • [1.1] VPCには先ほど作成した 「VPCtest」 を入れる
  • [1.2] アベイラビリティゾーンはとりあえず 「1a」 を選択
  • [1.3] IPv4 CIDRブロックは「10.0.1.0/24」
  • [2] プライベートtest作成
  • [2.1] [1.1][1.2]と同じ
  • [2.2] IPv4 CIDRブロックは「10.0.2.0/24」
  • [3] プライベートtest2作成
  • [3.1] [1.1]と同じ
  • [3.2] アベイラビリティゾーンは「1c」を選択([2.1]と異なるアベイラビリティゾーンならOK)
  • [3.3] IPv4 CIDRブロックは「10.0.200.0/24」

スクリーンショット 2020-09-27 13.34.18.png
スクリーンショット 2020-09-27 13.38.25.png
スクリーンショット 2020-09-27 17.40.58.png

これでサブネットの設定は完了です。

[1.1.3]ルートテーブル作成

ルートテーブルとは、サブネット内にあるインスタンス等がどこに通信にいくかのルールを定めたものです。つまり、ルートテーブルはパケットの宛先(IPアドレス)を見て、どこに通信を流すかが書かれている表です。この表をみてパケットを運ぶので、表にない宛先のものはパケットを送らないので、通信できません。サブネット毎にどこに通信ができるかを定めたものだというところがポイントです。 

作成手順

  • [1] ルートテーブルの作成
  • [2] 名前タグを 「ルートtest」
  • [3] VPCには先ほど作成した 「VPCtest」 を入れる

スクリーンショット 2020-09-27 13.50.30.png

次にインターネットゲートウェイの作成です。

[1.1.4]インターネットゲートウェイ作成

VPC内からインターネットに接続するためのゲートウェイです。これを使うことで、VPC内のシステムがグローバルIPを使えるようになります。

例えると自宅(VPC領域)に(インターネット)を入れる玄関のセキュリティ(鍵)の役割です。

作成手順

  • [1] インターネットゲートウェイ作成
  • [2] 名前タグを 「igw-test」 とする
  • [3] タグキーを「Name」 値を「igw-test」 とする
  • [4] VPCへアタッチする
  • [5] VPCには先ほど作成した 「VPCtest」 を入れる

スクリーンショット 2020-09-27 13.53.08.png
スクリーンショット 2020-09-27 13.53.24.png
スクリーンショット 2020-09-27 13.57.54.png

これでインターネットゲートウェイの設定は完了です。

[1.1.5]ルートテーブルに外部向けルートを追加

作成手順

  • [1] ルートの編集
  • [2] ルートの追加を押す
  • [3] 送信先は 「0.0.0.0/0」 にする
  • [4] ターゲットはInternet Gatewayから今回作成したインターネットゲートウェイを選択
  • [5] ルートの保存

スクリーンショット 2020-09-27 14.03.49.png
スクリーンショット 2020-09-27 14.06.17.png

これでルートテーブルの設定は完了です。

[1.1.6]サブネットにルートテーブルを紐付け

ルートテーブルの関連付けの編集を行います。

作成手順

  • [1] パブリックtestのルートテーブルの関連付けの編集
  • [2] ルートテーブルIDを今回作成した 「ルートtest」 に変更
  • [3] 保存

スクリーンショット 2020-09-27 14.11.14.png
スクリーンショット 2020-09-27 14.12.54.png

[1.2]EC2インスタンス作成

作成手順

  • [1] インスタンスを起動
  • [2] Amazon Linux2 AMI(HVM),SSD Volume Type を選択
  • [3] 無料利用枠のインスタンスタイプを選択後、次のステップ(詳細の設定)へ
  • [4] ネットワークを 「VPCtest」 、サブネットを 「パブリックtest」、 自動割り当てIPを 「有効」 後、次のステップ(ストレージ追加)へ
  • [5] 次のステップ(タグの追加)へ
  • [6] 別のタグを追加し、タグキーを「Name」 値を「webサーバーtest」 とした後、次のステップ(セキュリティグループ)へ
  • [7] 新しいセキュリティグループを作成し、名前を「webtest-sg」 とする
  • [7] 新しいキーペアの作成から キーペア名 「test-key」としキーペアのダウンロードを行う
  • [8] インスタンスの作成

スクリーンショット 2020-09-27 14.27.24.png
スクリーンショット 2020-09-27 14.30.28.png
スクリーンショット 2020-09-27 14.31.00.png
スクリーンショット 2020-09-27 14.36.09.png
スクリーンショット 2020-09-27 14.37.30.png
スクリーンショット 2020-09-27 14.38.53.png
スクリーンショット 2020-09-27 14.41.25.png
スクリーンショット 2020-09-27 14.42.22.png

EC2が起動できたか確認してみましょう
以下のコマンドを打ち込んでください

$ cd

$ cd (test-key.pemまでのPATH)
#意味がわからなければ $ cd Downloads でOKです

$ chmod 400 test-key.pem

$ ssh -i test-key.pem ec2-user@xxx.xxx.xxx.xxx
#xxxにはEC2のパブリックIPv4アドレスが入る

       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

#上記のEC2の文字が表示されれば起動完了です

[ec2-user] $ exit
#終了

これでEC2を起動することができました。

[1.3]RDSの作成

作成手順

  • [1] データベースの作成
  • [2] エンジンのタイプ :MySQL
  • [3] テンプレート :無料利用枠
  • [4] DBインスタンス識別子:database-test
  • [5] マスターユーザー名 :お好きな名前
  • [6] マスターパスワード :お好きなパスワード
  • [7] 接続VPC : VPCtest
  • [8] 追加の接続設定タップ
  • [9] VPCセキュリティグループ :新規作成
  • [10] VPCセキュリティグループ名 :dbtest-SG
  • [11] データベースの作成

スクリーンショット 2020-09-27 17.16.52.png

[1.3.2]セキュリティグループ編集

セキュリティグループについてまだ学習量が少なくセキュリティがガバガバな設定かもしれません。
テスト環境のみで使用してみてください。

イメージとしては自宅などに65535個(ポート番号)の玄関がありセキュリティグループで許可した玄関だけ鍵が解除されるという感じでしょうか?
- HTTP通信はポート80を使用
- HTTPS通信はポート443を使用
など代表的なポート番号などが存在します。

セキュリティグループwebtest-sgの編集を行います

作成手順

  • [1] セキュリティグループから「webtest-sg」のインバウンドルールを編集
  • [2] ルールを2つ追加する
  • [3] 1つはタイプ 「HTTP」、ソース 「0.0.0.0/0」 とする
  • [4] もう1つはタイプ 「MYSQL/Aurora」、ソース 「0.0.0.0/0」 とし、ルールを保存
  • [5] 「dbtest-SG」のインバウンドルールを編集
  • [6] タイプ 「MYSQL/Aurora」、ソース 「0.0.0.0/0」 とし、ルールを保存

(sgを小文字に統一するの忘れてた(;´Д`))

スクリーンショット 2020-09-27 20.35.45.png
スクリーンショット 2020-09-27 20.37.22.png
スクリーンショット 2020-09-27 20.24.41.png
スクリーンショット 2020-09-27 20.23.15.png

RDSが起動できたか確認してみましょう
以下のコマンドを打ち込んでください

$ cd

$ cd (test-key.pemまでのPATH)

$ ssh -i test-key.pem ec2-user@xxx.xxx.xxx.xxx

[ec2-user] $ sudo yum install mysql
# 「 Is this ok [y/d/N]: 」 と表示されるので 「y」 を入力

[ec2-user] $ mysql -h DBエンドポイント -u DBマスターユーザー名 -p
# パスワードを要求されるので入力(文字は表示されないが打ち込めています)(コピペ可)

>>> Welcome to the MariaDB monitor.
# が表示されればRDSのMySQLに接続ができた

#データベース内一覧を表示(小文字でも可 show database;)
MySQL > SHOW databases;

#「dbtest」という名前のデータベースを作成
MySQL > CREATE DATABASE dbtest;

#終了
MySQL > exit

これでRDSの設定は完了です。

[2]Django環境構築

[2.1]Python3をインストールするための準備

#yumをアップデート
[ec2-user] $ sudo yum update

#yumでgitをインストールします
[ec2-user] $ sudo yum install git -y

#次にgithubのリポジトリからpyenvをクローンしてきます
[ec2-user] $ git clone https://github.com/yyuu/pyenv.git ~/.pyenv

#パスを通してコマンドが打てるようにします
[ec2-user] $ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
[ec2-user] $ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
[ec2-user] $ echo 'eval "$(pyenv init -)"' >> ~/.bash_profile
[ec2-user] $ source ~/.bash_profile

#pyenvが入ったか確認
[ec2-user] $ pyenv -v
>>> pyenv 1.2.20-7-gdd62b0d1

[2.1.2]Python3のインストール

現在入っているPythonのバージョンの確認
Python 2系 は2020年1月1日にサービスを終了した
Python2系について詳しくはこの記事から

#Pythonのバージョンを確認
[ec2-user] $ python --version
>>> Python 2.7.18

#Python3に移行する

#必要な依存関係のインストール
[ec2-user] $ sudo yum install gcc zlib-devel bzip2 bzip2-devel readline readline-devel sqlite sqlite-devel openssl openssl-devel -y
>>>(略) 完了しました!

[ec2-user] $ sudo yum install libffi-devel

#現在ダウンロードできるPythonのバージョンを確認
[ec2-user] $ pyenv install -l
>>>(略) 
    3.8.5 
    3.8.6
   (略)


#現時点(R2,9/26)での最新版3.8.6をインストール
[ec2-user] $ pyenv install 3.8.6
>>> Downloading Python-3.8.6.tar.xz...
    -> https://www.python.org/ftp/python/3.8.6/Python-3.8.6.tar.xz
    Installing Python-3.8.6...
#少し時間がかかります
    Installed Python-3.8.6 to /home/ec2-user/.pyenv/versions/3.8.6


#Pythonのバージョン2系から3系へ切り替え
[ec2-user] $ pyenv global 3.8.6
[ec2-user] $ pyenv rehash

[ec2-user] $ python --version
>>> Python 3.8.6

#後に必要になってくるパッケージ
[ec2-user] $ sudo yum install python-devel mysql-devel
[ec2-user] $ pip install mysqlclient

#以上でPython3への移行が完了した

[2.2]Djangoの環境構築

pipはEC2(Linux2)に元から入っている

#pipバージョンの確認
[ec2-user] $ pip -V
>>> pip 20.2.1 from /home/ec2-user/.pyenv/versions/3.8.6/lib/python3.8/site-packages/pip (python 3.8)

#Djangoをインストール
[ec2-user] $ pip install django

#もし以下のエラーが出たら内容にしたがってシングルクォーテーション('xxxx')内をコピペして再度Djangoをインストールしてください
>>> WARNING: You are using pip version 20.2.1; however, version 20.2.3 is available.
You should consider upgrading via the 'xxxx' command.

#Djangoのバージョン確認
[ec2-user] $ python -m django --version

#「test」という名前のプロジェクトを作る
[ec2-user] $ django-admin startproject testDjango

確認すると生成されているはずである
[ec2-user] $ ls

>>> mysite/
        manage.py
        mysite/
            __init__.py
            settings.py
            urls.py
            asgi.py
            wsgi.py
  • トップのmysite・・・ルートディレクトリで任意の名前で作成できる。変更も可。
  • manage.py・・・Djangoプロジェクトの様々な操作を行うためのコマンドラインユーティリティ。
  • mysite・・・このプロジェクトのパッケージ。
  • mysite/init.py・・・このディレクトリがpythonであることの空ファイル。
  • mysite/settings.py・・・プロジェクトの設定ファイル。
  • mysite/urls.py・・・URLを宣言。
  • mysite/asgi.py・・・プロジェクトを提供するASGI互換WEBサーバーのエントリポイント。
  • mysite/wsgi.py・・・プロジェクトをサーブするためのWSGI互換WEBサーバーのエントリポイント。

[2.3]FileZillaをダウンロードする

testDjangoのコードを編集するためにSSHで直接vimを使って編集もできるが今回はFailZilaを使ってみようと思う
超初心者向け!FileZilla(ファイルジラ)の使い方

Win版FileZilaダウンロード
Mac版FileZilaダウンロード

インストールして開き,左上のサーバーボタンを押すと以下の画面が現れる

設定手順

  • [1] プロトコルは 「SFTP」 を選択 (FTPでは通信を暗号化していないため)
  • [2] ホストは 「EC2のオープンIPアドレス」 を入力
  • [3] ログオンタイプは 「鍵ファイル」
  • [4] ユーザー 「EC2にログインする時の名前」
  • [5] 鍵ファイル 「EC2にログインする時の鍵のPATH」
  • [6] ポート は空白でOK
  • [7] 接続

スクリーンショット 2020-09-26 13.10.24.png
スクリーンショット 2020-09-26 13.23.08.png

設定が完了したのち、上記の画面が現れる
左はローカルサイト(自分のPC内)
右はリモートサイト(EC2内)
ここでファイルの受け渡しや書き換えなどが行える。
とりあえず作ったtestDjangoをローカルサイトに複製をしておく

編集したいファイルを右クリック後、表示編集で編集が行える
次の[2.4]でPyCharmで編集できるようにする

[2.4]PyCharmをダウンロードする

最強のPython統合開発環境PyCharm

PyCharmを関連つけるには

  • [1] Finderから.pyのファイルを右クリック
  • [2] このアプリケーションで開くをPyCharmに設定
  • [3] すべてを変更

スクリーンショット 2020-09-26 13.35.35.png

[2.5]Django設定

[ec2-user] $ cd
[ec2-user] $ cd testDjango
[ec2-user] $ python manage.py startapp polls
settings.py
...
()
...

# DATABASES = {
#     'default': {
#         'ENGINE': 'django.db.backends.sqlite3',
#         'NAME': BASE_DIR / 'db.sqlite3',
#     }
# }

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'dbtest',
        'USER': 'DBマスターユーザー',
        'PASSWORD': 'DBマスターユーザーパスワード',
        'HOST': 'DBエンドポイント',
        'PORT': '3306',
    }
}

...
()
...

#LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = 'ja'

# TIME_ZONE = 'UTC'
TIME_ZONE = 'Asia/Tokyo'

FileZillaでアップロード

[ec2-user] $ cd
[ec2-user] $ cd testDjango
[ec2-user] $ python manage.py migrate
#ここでエラーが起きた場合考えられる原因は以下に記します。

[ec2-user] $ python manage.py dbshell

エラー原因

  • FileZillaを開いてアップロードしたか
  • 打ち込み忘れているコマンドはないか
  • errorのまま放置したダウンロード失敗のコマンドはないか
  • python --version で 3以上になっているか
  • [2.1.2]のコマンド打つ順番を守ったか
  • setting.pyでNAMEはMySQLで作成したDB名か(DB識別子ではない)

その他
pipenvのinstall時に、No module named '_ctypes'が発生する
CentOS7.2 + MySQL5.6 + Python3 - pip install mysqlclient で mysql_config not found が出る

もう無理だと感じた方は1からインスタンスを作り直すか、
EC2インスタンスを選択した状態でアクションを押し、同様のものを起動で
同じ設定のインスタンスが生成されます。

以上でEC2,RDS,Djangoの設定が完了しました。

おわりに

今回名前のつけ方が下手すぎました。( ´︵` )
たとえば、ブログの名前がxblogなら
xblog-vpc
xblog-public-subnet
xblog-ec2
xblog-rtb
xblog-ec2-sg
xblog-db-sg
などにした方がわかりやすいかと思います。

この記事で誰かの役に立っていただければ幸いです。

次回これにAPIを実装してみようと思います。

参考サイト

以下と本記事の途中に非常に参考にさせていただいたサイトを紹介しています。
ありがとうございました。

[1] EC2サーバにPython3環境構築
[2] djangoを用いてWEBアプリケーション開発 ~開発その1~
[3] AWS EC2 + RDS 環境で Django, syncdbするところまで
[4] EC2上のDjangoからAmazon RDSに接続する
[5] AWSのEC2(+RDS(+S3))でDjangoアプリケーション(+MySQL)を公開するまで
[6] (Djangoメモ)データベースにMySQLを設定
[7] MySQLでデータベースを作成しよう
[8] Django2.2で開発サーバー起動時にSQLite3のエラーが出た場合の対応
[9] Vim初心者に捧ぐ実践的入門
[10] AWSのルートテーブルって何よ?VPCとサブネット踏まえて簡単に説明してみる

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS VPC PrivateLink ~ demo

AWS PrivateLinkを利用して、他のAWS VPCに対して安全にサービスを提供する。

1.環境

AWSTemplateFormatVersion: '2010-09-09'
Description: 'For making 3 ec2 on 2 VPC. One VPC is a Client VPC which has 1 ec2.
  The other is a Provider VPC which has 2 ec2. '
Mappings:
  AWSInstanceType2Arch:
    t3.medium:
      Arch: HVM64
    t3.micro:
      Arch: HVM64
    t3.small:
      Arch: HVM64
    t3a.micro:
      Arch: HVM64
  AWSRegionArch2AMI:
    us-east-1:
      HVM64: ami-0a887e401f7654935
    us-west-2:
      HVM64: ami-0e8c04af2729ff1bb
Outputs:
  AZ:
    Description: Availability Zone of the Client EC2 instance
    Value: !Join
      - ''
      - - !GetAtt 'ClientInstance.AvailabilityZone'
  ClientIP:
    Description: Client EC2 IPaddress
    Value: !Join
      - ''
      - - !GetAtt 'ClientInstance.PublicIp'
  ProviderEC2One:
    Description: 'Web server #1 on ProviderVPC'
    Value: !Join
      - ''
      - - http://
        - !GetAtt 'ProviderInstanceOne.PublicIp'
  ProviderEC2Two:
    Description: 'Web server #2 on ProviderVPC'
    Value: !Join
      - ''
      - - http://
        - !GetAtt 'ProviderInstanceTwo.PublicIp'
Parameters:
  InstanceType:
    AllowedValues:
      - t3a.micro
      - t3.micro
      - t3.small
      - t3.medium
    ConstraintDescription: must be a valid EC2 instance type.
    Default: t3.micro
    Description: WebServer EC2 instance type
    Type: String
  SSHLocation:
    AllowedPattern: (\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})
    ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x.
    Default: '0.0.0.0/0'
    Description: ' The IP address range that can be used to SSH to the EC2 instances'
    MaxLength: '18'
    MinLength: '9'
    Type: String
Resources:
  AttachGateway:
    Properties:
      InternetGatewayId: !Ref 'InternetGateway'
      VpcId: !Ref 'ClientVPC'
    Type: AWS::EC2::VPCGatewayAttachment
  ClientInstance:
    Properties:
      ImageId: !FindInMap
        - AWSRegionArch2AMI
        - !Ref 'AWS::Region'
        - !FindInMap
          - AWSInstanceType2Arch
          - !Ref 'InstanceType'
          - Arch
      InstanceType: !Ref 'InstanceType'
      NetworkInterfaces:
        - AssociatePublicIpAddress: 'true'
          DeleteOnTermination: 'true'
          DeviceIndex: '0'
          GroupSet:
            - !Ref 'InstanceSecurityGroup'
          SubnetId: !Ref 'ClientSubnet'
      Tags:
        - Key: Application
          Value: !Ref 'AWS::StackId'
        - Key: Name
          Value: ClientEC2
      UserData: !Base64
        Fn::Join:
          - ''
          - - "#!/bin/bash -xe\n"
            - "yum update -y\n"
            - "\n"
    Type: AWS::EC2::Instance
  ClientSubnet:
    Properties:
      AvailabilityZone: us-west-2c
      CidrBlock: 10.100.0.0/24
      MapPublicIpOnLaunch: 'true'
      Tags:
        - Key: Application
          Value: !Ref 'AWS::StackId'
        - Key: Name
          Value: ClientPublic
      VpcId: !Ref 'ClientVPC'
    Type: AWS::EC2::Subnet
  ClientVPC:
    Properties:
      CidrBlock: 10.100.0.0/16
      EnableDnsHostnames: 'true'
      EnableDnsSupport: 'true'
      Tags:
        - Key: Application
          Value: !Ref 'AWS::StackId'
        - Key: Name
          Value: ClientVPC
    Type: AWS::EC2::VPC
  InboundResponsePortsNetworkAclEntry:
    Properties:
      CidrBlock: '0.0.0.0/0'
      Egress: 'false'
      NetworkAclId: !Ref 'NetworkAcl'
      PortRange:
        From: '1024'
        To: '65535'
      Protocol: '6'
      RuleAction: allow
      RuleNumber: '102'
    Type: AWS::EC2::NetworkAclEntry
  InboundSSHNetworkAclEntry:
    Properties:
      CidrBlock: '0.0.0.0/0'
      Egress: 'false'
      NetworkAclId: !Ref 'NetworkAcl'
      PortRange:
        From: '22'
        To: '22'
      Protocol: '6'
      RuleAction: allow
      RuleNumber: '101'
    Type: AWS::EC2::NetworkAclEntry
  InstanceSecurityGroup:
    Properties:
      GroupDescription: Enable SSH access via port 22
      SecurityGroupIngress:
        - CidrIp: !Ref 'SSHLocation'
          FromPort: '22'
          IpProtocol: tcp
          ToPort: '22'
      VpcId: !Ref 'ClientVPC'
    Type: AWS::EC2::SecurityGroup
  InternetGateway:
    Properties:
      Tags:
        - Key: Application
          Value: !Ref 'AWS::StackId'
    Type: AWS::EC2::InternetGateway
  NetworkAcl:
    Properties:
      Tags:
        - Key: Application
          Value: !Ref 'AWS::StackId'
      VpcId: !Ref 'ClientVPC'
    Type: AWS::EC2::NetworkAcl
  OutBoundHTTPNetworkAclEntry:
    Properties:
      CidrBlock: '0.0.0.0/0'
      Egress: 'true'
      NetworkAclId: !Ref 'NetworkAcl'
      PortRange:
        From: '80'
        To: '80'
      Protocol: '6'
      RuleAction: allow
      RuleNumber: '100'
    Type: AWS::EC2::NetworkAclEntry
  OutBoundResponsePortsNetworkAclEntry:
    Properties:
      CidrBlock: '0.0.0.0/0'
      Egress: 'true'
      NetworkAclId: !Ref 'NetworkAcl'
      PortRange:
        From: '1024'
        To: '65535'
      Protocol: '6'
      RuleAction: allow
      RuleNumber: '102'
    Type: AWS::EC2::NetworkAclEntry
  ProviderAttachGateway:
    Properties:
      InternetGatewayId: !Ref 'ProviderInternetGateway'
      VpcId: !Ref 'ProviderVPC'
    Type: AWS::EC2::VPCGatewayAttachment
  ProviderInboundHTTPNetworkAclEntry:
    Properties:
      CidrBlock: '0.0.0.0/0'
      Egress: 'false'
      NetworkAclId: !Ref 'ProviderNetworkAcl'
      PortRange:
        From: '80'
        To: '80'
      Protocol: '6'
      RuleAction: allow
      RuleNumber: '100'
    Type: AWS::EC2::NetworkAclEntry
  ProviderInboundResponsePortsNetworkAclEntry:
    Properties:
      CidrBlock: '0.0.0.0/0'
      Egress: 'false'
      NetworkAclId: !Ref 'ProviderNetworkAcl'
      PortRange:
        From: '1024'
        To: '65535'
      Protocol: '6'
      RuleAction: allow
      RuleNumber: '102'
    Type: AWS::EC2::NetworkAclEntry
  ProviderInboundSSHNetworkAclEntry:
    Properties:
      CidrBlock: '0.0.0.0/0'
      Egress: 'false'
      NetworkAclId: !Ref 'ProviderNetworkAcl'
      PortRange:
        From: '22'
        To: '22'
      Protocol: '6'
      RuleAction: allow
      RuleNumber: '101'
    Type: AWS::EC2::NetworkAclEntry
  ProviderInstanceOne:
    Metadata:
      AWS::CloudFormation::Init:
        config:
          files:
            /etc/cfn/cfn-hup.conf:
              content: !Join
                - ''
                - - "[main]\n"
                  - stack=
                  - !Ref 'AWS::StackId'
                  - "\n"
                  - region=
                  - !Ref 'AWS::Region'
                  - "\n"
              group: root
              mode: '000400'
              owner: root
            /etc/cfn/hooks.d/cfn-auto-reloader.conf:
              content: !Join
                - ''
                - - "[cfn-auto-reloader-hook]\n"
                  - "triggers=post.update\n"
                  - "path=Resources.ProviderInstanceOne.Metadata.AWS::CloudFormation::Init\n"
                  - 'action=/opt/aws/bin/cfn-init -v '
                  - '         --stack '
                  - !Ref 'AWS::StackName'
                  - '         --resource ProviderInstanceOne '
                  - '         --region '
                  - !Ref 'AWS::Region'
                  - "\n"
                  - "runas=root\n"
            /var/www/html/index.html:
              content: !Join
                - "\n"
                - - <img src="https://s3.amazonaws.com/cloudformation-examples/cloudformation_graphic.png"
                    alt="AWS CloudFormation Logo"/>
                  - <h1>Congratulations, you have successfully launched for AWS Summit
                    Tokyo Handson.</h1>
              group: root
              mode: '000644'
              owner: root
          packages:
            yum:
              httpd: []
          services:
            sysvinit:
              cfn-hup:
                enabled: 'true'
                ensureRunning: 'true'
                files:
                  - /etc/cfn/cfn-hup.conf
                  - /etc/cfn/hooks.d/cfn-auto-reloader.conf
              httpd:
                enabled: 'true'
                ensureRunning: 'true'
    Properties:
      ImageId: !FindInMap
        - AWSRegionArch2AMI
        - !Ref 'AWS::Region'
        - !FindInMap
          - AWSInstanceType2Arch
          - !Ref 'InstanceType'
          - Arch
      InstanceType: !Ref 'InstanceType'
      NetworkInterfaces:
        - AssociatePublicIpAddress: 'true'
          DeleteOnTermination: 'true'
          DeviceIndex: '0'
          GroupSet:
            - !Ref 'ProviderInstanceSecurityGroup'
          SubnetId: !Ref 'ProviderSubnet'
      Tags:
        - Key: Application
          Value: !Ref 'AWS::StackId'
        - Key: Name
          Value: Provider_1
      UserData: !Base64
        Fn::Join:
          - ''
          - - "#!/bin/bash -xe\n"
            - "yum update -y\n"
            - "yum update -y aws-cfn-bootstrap\n"
            - '/opt/aws/bin/cfn-init -v '
            - '         --stack '
            - !Ref 'AWS::StackName'
            - '         --resource ProviderInstanceOne '
            - '         --region '
            - !Ref 'AWS::Region'
            - "\n"
            - '/opt/aws/bin/cfn-signal -e $? '
            - '         --stack '
            - !Ref 'AWS::StackName'
            - '         --resource ProviderInstanceOne '
            - '         --region '
            - !Ref 'AWS::Region'
            - "\n"
    Type: AWS::EC2::Instance
  ProviderInstanceSecurityGroup:
    Properties:
      GroupDescription: Enable SSH access via port 22
      SecurityGroupIngress:
        - CidrIp: !Ref 'SSHLocation'
          FromPort: '22'
          IpProtocol: tcp
          ToPort: '22'
        - CidrIp: '0.0.0.0/0'
          FromPort: '80'
          IpProtocol: tcp
          ToPort: '80'
      VpcId: !Ref 'ProviderVPC'
    Type: AWS::EC2::SecurityGroup
  ProviderInstanceTwo:
    Metadata:
      AWS::CloudFormation::Init:
        config:
          files:
            /etc/cfn/cfn-hup.conf:
              content: !Join
                - ''
                - - "[main]\n"
                  - stack=
                  - !Ref 'AWS::StackId'
                  - "\n"
                  - region=
                  - !Ref 'AWS::Region'
                  - "\n"
              group: root
              mode: '000400'
              owner: root
            /etc/cfn/hooks.d/cfn-auto-reloader.conf:
              content: !Join
                - ''
                - - "[cfn-auto-reloader-hook]\n"
                  - "triggers=post.update\n"
                  - "path=Resources.ProviderInstanceOne.Metadata.AWS::CloudFormation::Init\n"
                  - 'action=/opt/aws/bin/cfn-init -v '
                  - '         --stack '
                  - !Ref 'AWS::StackName'
                  - '         --resource ProviderInstanceOne '
                  - '         --region '
                  - !Ref 'AWS::Region'
                  - "\n"
                  - "runas=root\n"
            /var/www/html/index.html:
              content: !Join
                - "\n"
                - - <img src="https://s3.amazonaws.com/cloudformation-examples/cloudformation_graphic.png"
                    alt="AWS CloudFormation Logo"/>
                  - <h1>Congratulations, you have successfully launched for AWS Summit
                    Tokyo Handson.</h1>
              group: root
              mode: '000644'
              owner: root
          packages:
            yum:
              httpd: []
          services:
            sysvinit:
              cfn-hup:
                enabled: 'true'
                ensureRunning: 'true'
                files:
                  - /etc/cfn/cfn-hup.conf
                  - /etc/cfn/hooks.d/cfn-auto-reloader.conf
              httpd:
                enabled: 'true'
                ensureRunning: 'true'
    Properties:
      ImageId: !FindInMap
        - AWSRegionArch2AMI
        - !Ref 'AWS::Region'
        - !FindInMap
          - AWSInstanceType2Arch
          - !Ref 'InstanceType'
          - Arch
      InstanceType: !Ref 'InstanceType'
      NetworkInterfaces:
        - AssociatePublicIpAddress: 'true'
          DeleteOnTermination: 'true'
          DeviceIndex: '0'
          GroupSet:
            - !Ref 'ProviderInstanceSecurityGroup'
          SubnetId: !Ref 'ProviderSubnet'
      Tags:
        - Key: Application
          Value: !Ref 'AWS::StackId'
        - Key: Name
          Value: Provider_2
      UserData: !Base64
        Fn::Join:
          - ''
          - - "#!/bin/bash -xe\n"
            - "yum update -y\n"
            - "yum update -y aws-cfn-bootstrap\n"
            - '/opt/aws/bin/cfn-init -v '
            - '         --stack '
            - !Ref 'AWS::StackName'
            - '         --resource ProviderInstanceTwo '
            - '         --region '
            - !Ref 'AWS::Region'
            - "\n"
            - '/opt/aws/bin/cfn-signal -e $? '
            - '         --stack '
            - !Ref 'AWS::StackName'
            - '         --resource ProviderInstanceTwo '
            - '         --region '
            - !Ref 'AWS::Region'
            - "\n"
    Type: AWS::EC2::Instance
  ProviderInternetGateway:
    Properties:
      Tags:
        - Key: Application
          Value: !Ref 'AWS::StackId'
    Type: AWS::EC2::InternetGateway
  ProviderNetworkAcl:
    Properties:
      Tags:
        - Key: Application
          Value: !Ref 'AWS::StackId'
      VpcId: !Ref 'ProviderVPC'
    Type: AWS::EC2::NetworkAcl
  ProviderOutBoundHTTPNetworkAclEntry:
    Properties:
      CidrBlock: '0.0.0.0/0'
      Egress: 'true'
      NetworkAclId: !Ref 'ProviderNetworkAcl'
      PortRange:
        From: '80'
        To: '80'
      Protocol: '6'
      RuleAction: allow
      RuleNumber: '100'
    Type: AWS::EC2::NetworkAclEntry
  ProviderOutBoundHTTPSNetworkAclEntry:
    Properties:
      CidrBlock: '0.0.0.0/0'
      Egress: 'true'
      NetworkAclId: !Ref 'ProviderNetworkAcl'
      PortRange:
        From: '443'
        To: '443'
      Protocol: '6'
      RuleAction: allow
      RuleNumber: '101'
    Type: AWS::EC2::NetworkAclEntry
  ProviderOutBoundResponsePortsNetworkAclEntry:
    Properties:
      CidrBlock: '0.0.0.0/0'
      Egress: 'true'
      NetworkAclId: !Ref 'ProviderNetworkAcl'
      PortRange:
        From: '1024'
        To: '65535'
      Protocol: '6'
      RuleAction: allow
      RuleNumber: '102'
    Type: AWS::EC2::NetworkAclEntry
  ProviderRoute:
    DependsOn: AttachGateway
    Properties:
      DestinationCidrBlock: '0.0.0.0/0'
      GatewayId: !Ref 'ProviderInternetGateway'
      RouteTableId: !Ref 'ProviderRouteTable'
    Type: AWS::EC2::Route
  ProviderRouteTable:
    Properties:
      Tags:
        - Key: Application
          Value: !Ref 'AWS::StackId'
      VpcId: !Ref 'ProviderVPC'
    Type: AWS::EC2::RouteTable
  ProviderSubnet:
    Properties:
      AvailabilityZone: us-west-2c
      CidrBlock: 10.100.0.0/24
      MapPublicIpOnLaunch: 'true'
      Tags:
        - Key: Application
          Value: !Ref 'AWS::StackId'
        - Key: Name
          Value: ProviderPublic
      VpcId: !Ref 'ProviderVPC'
    Type: AWS::EC2::Subnet
  ProviderSubnetNetworkAclAssociation:
    Properties:
      NetworkAclId: !Ref 'ProviderNetworkAcl'
      SubnetId: !Ref 'ProviderSubnet'
    Type: AWS::EC2::SubnetNetworkAclAssociation
  ProviderSubnetRouteTableAssociation:
    Properties:
      RouteTableId: !Ref 'ProviderRouteTable'
      SubnetId: !Ref 'ProviderSubnet'
    Type: AWS::EC2::SubnetRouteTableAssociation
  ProviderVPC:
    Properties:
      CidrBlock: 10.100.0.0/16
      EnableDnsHostnames: 'true'
      EnableDnsSupport: 'true'
      Tags:
        - Key: Application
          Value: !Ref 'AWS::StackId'
        - Key: Name
          Value: ProviderVPC
    Type: AWS::EC2::VPC
  Route:
    DependsOn: AttachGateway
    Properties:
      DestinationCidrBlock: '0.0.0.0/0'
      GatewayId: !Ref 'InternetGateway'
      RouteTableId: !Ref 'RouteTable'
    Type: AWS::EC2::Route
  RouteTable:
    Properties:
      Tags:
        - Key: Application
          Value: !Ref 'AWS::StackId'
      VpcId: !Ref 'ClientVPC'
    Type: AWS::EC2::RouteTable
  SMInstanceProfile:
    Properties:
      Path: /
      Roles:
        - !Ref 'SMRole'
    Type: AWS::IAM::InstanceProfile
  SMRole:
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action:
              - sts:AssumeRole
            Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
        Version: '2012-10-17'
      Path: /
      Policies:
        - PolicyDocument:
            Statement:
              - Action:
                  - ssm:UpdateInstanceInformation
                  - ssmmessages:CreateControlChannel
                  - ssmmessages:CreateDataChannel
                  - ssmmessages:OpenControlChannel
                  - ssmmessages:OpenDataChannel
                Effect: Allow
                Resource: '*'
              - Action:
                  - s3:GetEncryptionConfiguration
                Effect: Allow
                Resource: '*'
            Version: '2012-10-17'
          PolicyName: root-0317a
    Type: AWS::IAM::Role
  SubnetNetworkAclAssociation:
    Properties:
      NetworkAclId: !Ref 'NetworkAcl'
      SubnetId: !Ref 'ClientSubnet'
    Type: AWS::EC2::SubnetNetworkAclAssociation
  SubnetRouteTableAssociation:
    Properties:
      RouteTableId: !Ref 'RouteTable'
      SubnetId: !Ref 'ClientSubnet'
    Type: AWS::EC2::SubnetRouteTableAssociation

2.NLBの作成とVPC エンドポイントサービスを設定する

2-1.NLBを作成する

・ロードバランサーの設定
VPCは Provider-VPCを選択
スクリーンショット 2020-09-28 110427.png

・セキュリティ設定の構成
SSLは利用しないので、そのまま進む。

・ルーティングの設定
ターゲットグループ名だけ入力し、後はデフォルトとする。

・ターゲットの登録
LBさせる2つのインスタンスを追加。
スクリーンショット 2020-09-28 111137.png

・確認
完了。

2-2.VPC エンドポイントサービスを作成する

VPCの「エンドポイントサービスの作成」から「2-1.NLBを作成する」で作成したNLBを紐づける。
スクリーンショット 2020-09-28 111137.png

3.VPC エンドポイントを作成する

「2.VPC エンドポイントサービス設定を作成し、NLBを紐づける」で作成したエンドポイントサービスをClientPublicのVPCを指定する。
スクリーンショット 2020-09-28 114110.png

セキュリティグループは、VPC内からのHTTP通信を受け付けるようにする。
スクリーンショット 2020-09-28 114635.png

エンドポイントサービス側に戻り、承認作業を行う。
スクリーンショット 2020-09-28 114931.png

4.動作確認

Client側のEC2でEC2 Instance Connectで接続し、エンドポイントにcurlで接続する。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS Lambdaも満足に書けないインフラエンジニアがAWS認定DevOpsエンジニアプロフェッショナル (DOP)を受験して合格してみた

AWS Lambdaも満足に書けないインフラエンジニアの身でありながら
AWS認定DevOpsエンジニアプロフェッショナルを受験して合格したので
勉強方法をまとめてみます。

受験した人

・AWS歴4年半
・インフラエンジニアではあるが、AWS CLIも満足につかえず基本的にマネジメントコンソール でぽちぽちしている
・AWS CLIも使えないので当然、AWS Lambdaなんて書けるわけもない。
・業務においてAWSにて本番ワークロードで稼働中のサーバ運用や新規構築を担当している。

AWS認定試験受験履歴

資格名 取得日
AWS Certified Solutions Architect – Associate (SAA) 2016/ 8/29取得
AWS Certified SysOps Administrator – Associate (SOA) 2017/ 5/21取得
AWS Certified Developer – Associate (DVA) 2018/11/17取得
AWS Certified Solutions Architect – Professional (SAP) 2018/ 2/13取得

今回、受験したDevOpsエンジニアプロフェッショナルの立ち位置は以下になります。

スクリーンショット 2020-09-28 11.21.39.png
参照:https://aws.amazon.com/jp/certification/

今回はSOAの有効期限が近づいてきたので、更新も兼ねてDevOpsを受験してみました。
(ほんとは2020/5/21に失効だったけど、コロナの影響で半年有効期限が伸びたのでギリギリの取得です。)

勉強方法

DevOpsエンジニアプロフェッショナルの参考書はでていないので、
セオリー通り以下の勉強を実施しました。

・AWSドキュメント熟読
・試験範囲のAWSサービスについてホワイトペーパの熟読
・試験範囲のAWSサービスについてBlackBeltを熟読
・AWS公式のサンプル問題
・AWS公式の模擬試験

嘘です・・・・。
ホワイトペーパなんて1分たりとも読んでません。
読んだらヨダレ垂らして寝ちゃいます・・・。

勉強方法(真)

ほんとはこっち。
教科書を読むのが苦手なので、勉強は実戦形式で問題をといて
不明点のみBlackbeltやAWSドキュメントを参照しました。

・AWS公式デジタルトレーニング
 https://aws.amazon.com/jp/certification/certification-prep/
・AWS WEB問題集で学習しよう
 https://aws.koiwaclub.com/
・Udemy
 https://www.udemy.com/course/aws-devops-dop-c01-2020/
・AWS公式の模擬試験
 https://aws.amazon.com/jp/certification/certified-devops-engineer-professional/

AWS公式デジタルトレーニングが中でもかなりわかりやすかったです。
音声は英語ですが、日本語字幕で説明してくれて、資料も日本語です。
カルキュラムも日本語で各カテゴリごとポイントを紹介があり、練習問題もあり見やすいです。
全体で7時間とありますが、カテゴリごとに見ていけばそんなに時間がかからないイメージです。

受験した感想

「AWS WEB問題集で学習しよう」や「Udemy」で問題集をといていくと問題のパターンがある程度みえてくるので
75問に対し、80分ほどで一通回答が完了し、残りの時間は見直しに費やす予定でした。

しかし現実はそう甘くなく、75問解いた段階で、120分使っていました。
そこから回答に迷った問題の見直し、再度問題の1問目から見直しを行う予定でしたが、
時間が足りず、全体の半分ほどしか見直しが行えませんでした。
(やはりProfessionalレベルは時間が厳しい・・・)

Professional試験は問題文も選択肢も共に長文が出題される傾向にあるので、
何を問われているかをポイントを見極める訓練が必要かと思いました。

まとめ

勉強している最中は、はじめてProfessionalレベルの問題を解いた時よりは絶望感はありませんでしたが、
やはり本番の試験となると苦戦しました。(30問目あたりから・・・。)

個人的にはSAPやDVAよりは難易度は簡単に感じました。

ただ、私のようにdev系のナレッジのない人間でも、合格はできましたので、
受験を考えている方は学習方法を参考にしていただければと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MacとSSHでさくっとIPアドレスと国判定を変える

Amazonプライムである作品が「このビデオは、現在、 お住まいの地域では視聴できません」となっていました。ではアメリカからアクセスしたら利用可能になるのか調べてみました。

今回はAWSのLightsailを使い、リージョンをus-east-1にすることで、バージニア州からのアクセスとすることを目標とします。
サーバーと秘密鍵がスタンバイできたら、以下のコマンドでSSHしましょう。ホスト名(IP)と秘密鍵のパス等は置き換えてください。

ssh ec2-user@10.87.131.119 -i ~/Downloads/LightsailDefaultKey-us-east-1.pem -D 10000

もしSSHが失敗し、原因がダウンロードしたての秘密鍵を利用して以下のようなWarningが出たことによるものであったら、
chmod 600 ~/Downloads/LightsailDefaultKey-us-east-1.pem
で解消します。

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@         WARNING: UNPROTECTED PRIVATE KEY FILE!          @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for '/Users/umihico/Downloads/LightsailDefaultKey-us-east-1.pem' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.

無事SSHが成功している間に、別のターミナルを開いて以下を実行します。
networksetup -setsocksfirewallproxy Wi-fi localhost 10000
このコマンドはMacでWi-Fiを利用中の場合の設定になります。もとに戻したいときは以下のコマンドです。インスタンスの削除も忘れずに。
networksetup -setsocksfirewallproxystate Wi-fi off

ipinfo.ioでも無事に変更が確認できました。
しかし、Amazonプライムの該当作品は引き続き同じ文言で視聴できませんでした\(^o^)/
IP以前にamazon.co.jpである時点で弾くのが正しいですよね。多分そうなっているんでしょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ServerlessFramework(AWS)でstage毎にS3などのリソースを別に作る方法

provider > stage: ${opt:stage, self:custom.defaultStage}
custom > defaultStage: dev
custom > resourcePrefix : ${self:provider.stage}を使った値
と設定して
BucketNameを ${self:custom.resourcePrefix}-resourceとする

serverles..yml
service: serverless-sample

frameworkVersion: '2'

provider:
  name: aws
  runtime: python3.8
  stage: ${opt:stage, self:custom.defaultStage} 
  region: ${opt:provider, self:custom.defaultRegion}

custom:
  author: iwato
  defaultStage: dev
  defaultRegion: ap-northeast-1
  resourcePrefix: ${self:custom.author}-${self:service}-${self:provider.stage}
  environment:
    dev: # 開発環境
      key: dev-value
    test: # テスト環境
      key: test-value
    staging: # 検証環境
      key: staging-value
    prod: # 本番環境
      prod: prod-value

resources:
  Resources:
    NewResource:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:custom.resourcePrefix}-resource
  Outputs:
     NewOutput:
       Description: "Description for the output"
       Value: "Some output value"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LineからEC2を操作する

AWS EC2上で自作のアプリケーションを回しているのですが、実行、停止を頻繁に繰り返すことがあり、EC2に入っていちいち操作させるのが結構めんどくさかったりします。
そこで、LineからEC2を操作して、プログラムの実行停止をできるようにしてみます。

全体像

以下のフローでLineからEC2を操作します。
1. Line Messaging APIからAPI GatewayへPOSTリクエストを送信
2. API Gatewayをトリガにして、Lambda関数が実行
3. Lambda関数からEC2インスタンスが起動or停止される
4. 起動時には設定したスクリプトが自動実行される
スクリーンショット 2020-09-28 0.10.54.png

作成手順

  1. EC2インスタンスの作成
  2. IAMポリシーの作成
  3. Lambda関数の設定
  4. API Gatewayの設定
  5. Line Messaging APIの設定
  6. EC2起動時実行スクリプトの設定

1. EC2インスタンスの作成

プログラムを実行するためのEC2インスタンスを作成しておきます。(作成手順は省略)
後述のLambda関数で利用するため、作成したEC2のインスタンスIDをメモしておきます。

2. Lambda用IAMロールの作成

今回はLambdaの関数からEC2を操作するため、LambdaにEC2インスタンスの起動停止を行うための、IAMロールを付与してあげます。
こちらのAWS公式マニュアルに従います。

【IAMポリシーの作成】
IAM -> ポリシー -> ポリシーの作成 -> JSON
でエディタに下記のJSONを記載します。
ポリシー名はec2StartStopPolicyとします。

ポリシードキュメント
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:Start*",
        "ec2:Stop*"
      ],
      "Resource": "*"
    }
  ]
}

【IAMロールの作成】
ロール -> ロールの作成 を選択し、エンティティの種類にAWSサービス、ユースケースにLambdaを選択します。
アクセス権限ポリシーで、上で作成したポリシーを選択し、ロール名をec2StartStopRoleとして、ロールを作成します。

3. Lambda関数の作成

EC2を起動、停止するためのLambda関数を作成します。
【関数の作成】
Lineから「起動」「停止」というメッセージを受け取って、対応するアクションを行う仕様とします。

Lambda -> 関数の作成 -> 一から作成 -> ランタイムにPython3.8を選択 -> 関数の作成

配置されたLambda関数の関数コード内、lambda_function.pyに下記のコードを記載します。
コード内のregionとinstancesには対象のEC2インスタンスの内容に置き換えてください。

Lambda関数
lambda_function.py
import json
import urllib.request, urllib.parse
import boto3
import os
region = '**リージョン**'
instances = ['**インスタンスID**']
ec2 = boto3.client('ec2', region_name=region)


def start_ec2():
    ec2.start_instances(InstanceIds=instances)

def stop_ec2():
    ec2.stop_instances(InstanceIds=instances)


def lambda_handler(event, context):
    # TODO implement
    text = event['events'][0]['message']['text']
    if text == '起動':
        start_ec2()
    elif text == '停止':
        stop_ec2()
    else:
        pass

    print("Line Message is {}".format(text))
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

【IAMロールを付与】
作成したIAMロールをLambda関数に付与します。
Lambdaのアクセス権限タブで実行ロールの編集をクリックし、先ほど作成したIAMロールを選択して保存します。

4. API Gatewayの設定

作成したLambdaのデザイナー上から、トリガーを追加を選択し、API Gatewayをトリガーとして追加します。

作成したAPI GatewayのAPIエンドポイントをメモしておきます。

今回は、LineからのPOSTリクエストに応答して、Lambda関数をトリガするようにします。
API Gateway画面から、アクション -> メソッドを追加 -> POSTを選択 -> 作成したLambda関数を指定して、POSTリクエスト用のメソッドを作成します。

最後に、アクション->APIのデプロイ を選択し、デプロイします。

4. Line Messaging APIの設定

API GatewayにPOSTリクエストを送るためのLine Messaging APIを作成します。
Line Developers出ない方は新規登録をしましょう。

【チャネルの作成】
Line Developersにログインし、新規Providerを作成します。
Channelsに「Create a MEssaging API channel」を選択し、Channel nameなど適当に埋めて、Channelを作成します。

【Webhook URLの設定】
Messaging APIの設定で、Webhook URL選択し、作成したAPI GatewayのAPIエンドポイントを記載します。

【動作確認】
Messaging API設定画面で表示されるQRコードを読み取り、Lineで友達追加します。
このチャネルで、「起動」または「停止」を送って、実際にEC2インスタンスが起動、停止することを確かめます。
起動、停止を送ると、、、

EC2インスタンスに反映!
スクリーンショット 2020-09-27 18.40.32.png

5. 起動時設定

最後に、EC2が起動したときに特定のスクリプトを実行できるようにします。
今回は起動・停止時にメッセージを出力するだけの、mytestというスクリプトを登録します。

mytest
#!/bin/sh
# chkconfig: 2345 99 10
# description: test shell

case "$1" in
 start)
       echo "start!" > /tmp/start.txt
       ;;
 stop)
       echo "stop!" > /tmp/stop.txt
       ;;
  *) break ;;
esac

下記のコマンドでchkconfigに登録すると、スクリプトが実行されるようになります。

$ chkconfig --add mytest

$ chkconfig mytest on

まとめ

以上で、好きなタイミングでLineからEC2を起動させプログラムを自動起動できるようになりました!外出中でも気軽にEC2を操作できるのが嬉しいですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】AWS-CDKを利用してBatch環境を作成する

1. はじめに

今回は、AWS Batchの環境をCDKを利用して実装していきます。
よくTypeScriptでの実装例は多いのですが、Pythonはあまりなかったので記事にしました。

1.1 実行環境

実行環境は以下の通りです。
特にインストールやaws-cliとaws-cdkの初期設定については触れません。
ただ、注意点としてaws-cdkは非常にバージョンの更新頻度が高く、現在書かれている内容でも動かない可能性があります。

  • MacOS: Catarila (10.15.6)
  • Python (brew): 3.8
  • aws-cli (brew): 2.0.50
  • aws-cdk (brew): 1.63.0 (build 7a68125)
  • docker: 19.03.12

1.2 料金

気になるのは、料金ですよね。
以下の条件で動かしたところ、課金されたのはEC2の料金のみで0.01 [$/日]程度でした。
(Batchだと、毎回ジョブにキューが追加されてからインスタンスの作成が行われ、ジョブが完了すると削除されるためです。)

  • 処理時間:10~15 [min/回]
  • 起動したインスタンスタイプ: c4.large (スポット料金)

1.3 手順

Batchの実行環境を整えるのに以下の手順で実装を行います。

  1. Python scriptの作成
  2. Dockerfileの作成
  3. ECRに登録
  4. CDKにてappを記述

1.4 準備

フォルダ構成は以下の通りです。
ファイル名の右側に付いているのは、上記の手順の番号と対応しています。

batch_example
└── src
    ├── docker
    │   ├── __init__.py (1)
    │   ├── Dockerfile (2)
    │   ├── requirements.txt (2)
    │   └── Makefile (3)
    └── batch_environment
        ├── app.py (4)
        ├── cdk.json
        └── README.md

2. 実装

それでは、上記の手順にしたがって実装を進めていきます。

2.1 Python scriptの作成

Docker内にて実行するscriptの例を以下に示します。
clickはCMDからのコマンドライン引数の受け渡しをするために、
watchtowerはCloudWatch Logsへのログの書き込みをするために利用しています。

__init__.py
# timeのparse用
from datetime import datetime
from logging import getLogger, INFO
# インストールライブラリ
from boto3.session import Session
import click
import watchtower


# envvarで指定すると環境変数から値を取得する
@click.command()
@click.option("--time")
@click.option("--s3_bucket", envvar='S3_BUCKET')
def main(time: str, s3_bucket: str):
    if time:
        # CloudWatch Eventから実行することを想定し、時刻のparseをする
        d = datetime.strptime(time, "%Y-%m-%dT%H:%M:%SZ")
        # 実行日付を取得
        execute_date = d.strftime("%Y-%m-%d")

    # loggerの設定
    # loggerの名前がログストリームの名前になる
    logger_name = f"{datetime.now().strftime('%Y/%m/%d')}"
    logger = getLogger(logger_name)
    logger.setLevel(INFO)
    # CloudWatch Logsのロググループの名前をここで指定
    # Sessionを渡してIAM Role経由でログを送信
    handler = watchtower.CloudWatchLogHandler(log_group="/aws/some_project", boto3_session=Session())
    logger.addHandler(handler)

    # 実行予定の処理
    # ここでは、CloudWatch Logsに実行日時を書き込むのみ
    logger.info(f"{execute_date=}")

if __name__ == "__main__":
    """
    python __init__.py 
        --time 2020-09-11T12:30:00Z
        --s3_bucket your-bucket-here
    """
    main()

2.2 Dockerfileの作成

次に上記のPython scriptを実行するDockerfileを作成します。
ここを参考にマルチステージでビルドしました。

Dockerfile
# ここはビルド用のコンテナ
FROM python:3.8-buster as builder

WORKDIR /opt/app

COPY requirements.txt /opt/app
RUN pip3 install -r requirements.txt

# ここからは実行用コンテナの準備
FROM python:3.8-slim-buster as runner

COPY --from=builder /usr/local/lib/python3.8/site-packages /usr/local/lib/python3.8/site-packages
COPY src /opt/app/src

WORKDIR /opt/app/src
CMD ["python3", "__init__.py"]

同時にrequirements.txtにも利用するライブラリを入れておきます。

requirements.txt
click
watchtower

2.3 ECRへの登録

Dockerfileの作成が終わったら、ECRに登録します。
まずは、コンソールからECRに「リポジトリ作成」ボタンを押してrepositoryを作成します。

スクリーンショット 2020-09-27 22.47.35.png

リポジトリの名前は適当に設定します。

スクリーンショット 2020-09-27 22.49.17.png

作成したリポジトリを選択して、「プッシュコマンドの表示」ボタンを押します。

スクリーンショット 2020-09-27 22.50.23.png

すると、プッシュに必要なコマンドが表示されるので、何も考えずにコピーして実行していきます。
ここで、失敗する方はAWS CLIの設定がうまくで来ていないと思うので、AWS CLIの設定を見直してください。

スクリーンショット 2020-09-27 22.51.53.png

毎回、コマンドを打つのが大変なので、上記のコマンドをコピーしたMakefileを作成します。
(1のコマンドの--username AWSは定数だと思われます。)

Makefile
.PHONY: help
help:
    @echo " == push docker image to ECR == "
    @echo "type 'make build tag push' to push docker image to ECR"
    @echo ""

.PHONY: login
login:
    (1のコマンド)aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin {ACCOUNT_NUMBER}.dkr.ecr.ap-northeast-1.amazonaws.com

.PHONY: build
build:
    (2のコマンド)docker build -t {REPOSITORY_NAME} .

.PHONY: tag
tag:
    (3のコマンド)docker tag {REPOSITORY_NAME}:latest {ACCOUNT_NUMBER}.dkr.ecr.ap-northeast-1.amazonaws.com/{REPOSITORY_NAME}:latest

.PHONY: push
push:
    (4のコマンド)docker push {ACCOUNT_NUMBER}.dkr.ecr.ap-northeast-1.amazonaws.com/{REPOSITORY_NAME}:latest

このMakefileを利用することで、以下のように簡単にコマンドを短縮できます。
加えて、上記のMakefileには特に外部にもれても危ない情報はないと思うので、ソースコードも共有できます。

# ECRにログイン
$ make login

# ECRに最新の状態のimageをpush
$ make build tag push

2.4 CDKの実装

CDKの実装内容はTypeScriptで書かれたこの記事を参考に行いました。
なお、事前にapp.pyを実装するディレクトリにて$ cdk initの実行をした方が良いです。

2.4.1 実装に必要なパッケージのインストール

一つ一つのパッケージ名が長いですね・・・。加えて、インストールにかかる時間も結構長いです。

$ pip install aws-cdk-core aws-cdk-aws-stepfunctions aws-cdk-aws-stepfunctions-tasks aws-cdk-aws-events-targets aws-cdk.aws-ec2 aws-cdk.aws-batch aws-cdk.aws-ecr

2.4.2 app.pyの実装

まずは、今回構築する環境のクラスを作成します。
BatchEnvironmentクラスの引数として、stack_namestack_envを設定しています。
これは、この環境の名前と、実行環境(検証/開発/本番)などと対応しています。
(なお、本当に実行環境を分ける場合は、ECRのリポジトリも変更する必要があるかなと思います。)

app.py
from aws_cdk import (
    core,
    aws_ec2,
    aws_batch,
    aws_ecr,
    aws_ecs,
    aws_iam,
    aws_stepfunctions as aws_sfn,
    aws_stepfunctions_tasks as aws_sfn_tasks,
    aws_events,
    aws_events_targets,
)


class BatchEnvironment(core.Stack):
    """
    Batchの環境とそれを実行するStepFunctions + CloudWatch Event環境を作成

    """
    # 上で作成したECRのリポジトリ名
    # Batchで実行する際に、このリポジトリからイメージをpullする
    ECR_REPOSITORY_ARN = "arn:aws:ecr:ap-northeast-1:{ACCOUNT_NUMBER}:repository/{YOUR_REPOSITORY_NAME}"

    def __init__(self, app: core.App, stack_name: str, stack_env: str):
        super().__init__(scope=app, id=f"{stack_name}-{stack_env}")
        # 以下の実装はここの下に連なるイメージです。

2.4.3 app.pyの実装(VPC環境の作成)

app.py
        # def __init__(...):の中

        # CIDRは好きな範囲を
        cidr = "192.168.0.0/24"

        # === #
        # vpc #
        # === #
        # VPCはパブリックサブネットしか利用しない場合は、無料で利用可能できる(はずです)
        vpc = aws_ec2.Vpc(
            self,
            id=f"{stack_name}-{stack_env}-vpc",
            cidr=cidr,
            subnet_configuration=[
                # Public Subnetのnetmaskを定義
                aws_ec2.SubnetConfiguration(
                    cidr_mask=28,
                    name=f"{stack_name}-{stack_env}-public",
                    subnet_type=aws_ec2.SubnetType.PUBLIC,
                )
            ],
        )

        security_group = aws_ec2.SecurityGroup(
            self,
            id=f'security-group-for-{stack_name}-{stack_env}',
            vpc=vpc,
            security_group_name=f'security-group-for-{stack_name}-{stack_env}',
            allow_all_outbound=True
        )

        batch_role = aws_iam.Role(
            scope=self,
            id=f"batch_role_for_{stack_name}-{stack_env}",
            role_name=f"batch_role_for_{stack_name}-{stack_env}",
            assumed_by=aws_iam.ServicePrincipal("batch.amazonaws.com")
        )

        batch_role.add_managed_policy(
            aws_iam.ManagedPolicy.from_managed_policy_arn(
                scope=self,
                id=f"AWSBatchServiceRole-{stack_env}",
                managed_policy_arn="arn:aws:iam::aws:policy/service-role/AWSBatchServiceRole"
            )
        )

        batch_role.add_to_policy(
            aws_iam.PolicyStatement(
                effect=aws_iam.Effect.ALLOW,
                resources=[
                    "arn:aws:logs:*:*:*"
                ],
                actions=[
                    "logs:CreateLogGroup",
                    "logs:CreateLogStream",
                    "logs:PutLogEvents",
                    "logs:DescribeLogStreams"
                ]
            )
        )

        # EC2に付与するRole
        instance_role = aws_iam.Role(
            scope=self,
            id=f"instance_role_for_{stack_name}-{stack_env}",
            role_name=f"instance_role_for_{stack_name}-{stack_env}",
            assumed_by=aws_iam.ServicePrincipal("ec2.amazonaws.com")
        )

        instance_role.add_managed_policy(
            aws_iam.ManagedPolicy.from_managed_policy_arn(
                scope=self,
                id=f"AmazonEC2ContainerServiceforEC2Role-{stack_env}",
                managed_policy_arn="arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role"
            )
        )

        # S3にアクセスするpolicyの追加
        instance_role.add_to_policy(
            aws_iam.PolicyStatement(
                effect=aws_iam.Effect.ALLOW,
                resources=["*"],
                actions=["s3:*"]
            )
        )

        # CloudWatch Logsにアクセスするpolicyの追加
        instance_role.add_to_policy(
            aws_iam.PolicyStatement(
                effect=aws_iam.Effect.ALLOW,
                resources=[
                    "arn:aws:logs:*:*:*"
                ],
                actions=[
                    "logs:CreateLogGroup",
                    "logs:CreateLogStream",
                    "logs:PutLogEvents",
                    "logs:DescribeLogStreams"
                ]
            )
        )

        # EC2にロールを付与
        instance_profile = aws_iam.CfnInstanceProfile(
            scope=self,
            id=f"instance_profile_for_{stack_name}-{stack_env}",
            instance_profile_name=f"instance_profile_for_{stack_name}-{stack_env}",
            roles=[instance_role.role_name]
        )

 2.4.4 app.pyの実装(Batchの実行環境およびジョブ定義・ジョブキューの作成)

app.py
        # VPCの続き...

        # ===== #
        # batch #
        # ===== #
        batch_compute_resources = aws_batch.ComputeResources(
            vpc=vpc,
            maxv_cpus=4,
            minv_cpus=0,
            security_groups=[security_group],
            instance_role=instance_profile.attr_arn,
            type=aws_batch.ComputeResourceType.SPOT
        )

        batch_compute_environment = aws_batch.ComputeEnvironment(
            scope=self,
            id=f"ProjectEnvironment-{stack_env}",
            compute_environment_name=f"ProjectEnvironmentBatch-{stack_env}",
            compute_resources=batch_compute_resources,
            service_role=batch_role
        )

        job_role = aws_iam.Role(
            scope=self,
            id=f"job_role_{stack_name}-{stack_env}",
            role_name=f"job_role_{stack_name}-{stack_env}",
            assumed_by=aws_iam.ServicePrincipal("ecs-tasks.amazonaws.com")
        )

        job_role.add_managed_policy(
            aws_iam.ManagedPolicy.from_managed_policy_arn(
                scope=self,
                id=f"AmazonECSTaskExecutionRolePolicy_{stack_name}-{stack_env}",
                managed_policy_arn="arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"

            )
        )

        job_role.add_managed_policy(
            aws_iam.ManagedPolicy.from_managed_policy_arn(
                scope=self,
                id=f"AmazonS3FullAccess_{stack_name}-{stack_env}",
                managed_policy_arn="arn:aws:iam::aws:policy/AmazonS3FullAccess"

            )
        )

        job_role.add_managed_policy(
            aws_iam.ManagedPolicy.from_managed_policy_arn(
                scope=self,
                id=f"CloudWatchLogsFullAccess_{stack_name}-{stack_env}",
                managed_policy_arn="arn:aws:iam::aws:policy/CloudWatchLogsFullAccess"
            )
        )

        batch_job_queue = aws_batch.JobQueue(
            scope=self,
            id=f"job_queue_for_{stack_name}-{stack_env}",
            job_queue_name=f"job_queue_for_{stack_name}-{stack_env}",
            compute_environments=[
                aws_batch.JobQueueComputeEnvironment(
                    compute_environment=batch_compute_environment,
                    order=1
                )
            ],
            priority=1
        )

        # ECRリポジトリの取得
        ecr_repository = aws_ecr.Repository.from_repository_arn(
            scope=self,
            id=f"image_for_{stack_name}-{stack_env}",
            repository_arn=self.ECR_REPOSITORY_ARN
        )

        # ECRからイメージの取得
        container_image = aws_ecs.ContainerImage.from_ecr_repository(
            repository=ecr_repository
        )

        # ジョブ定義
        # ここで、Python scriptで利用する`S3_BUCKET`を環境変数として渡す
        batch_job_definition = aws_batch.JobDefinition(
            scope=self,
            id=f"job_definition_for_{stack_env}",
            job_definition_name=f"job_definition_for_{stack_env}",
            container=aws_batch.JobDefinitionContainer(
                image=container_image,
                environment={
                    "S3_BUCKET": f"{YOUR_S3_BUCKET}"
                },
                job_role=job_role,
                vcpus=1,
                memory_limit_mib=1024
            )
        )


2.4.5 app.pyの実装(StepFunctions + CloudWatch Eventsの作成)

ここからは、必ずしもBatchの環境構築に必要ではありませんが、
定期実行をするためにStepFunctionsとCloudWatch Eventを利用して行います。

CloudWatch Eventからも直接Batchを呼べますが、
他サービスとの連携のしやすさやパラメータの受け渡しなどを考えて間にStepFunctionsを挟んでいます。

StepFunctionsのステップとして登録する際に、
DockerのCMDコマンドを(=Batchのジョブ定義に設定した状態)上書きして、
CloudWatch Eventからの引数timeを受け取り、Python scriptへ渡しています。

app.py
        # Batchの続き...

        # ============= #
        # StepFunctions #
        # ============= #

        command_overrides = [
            "python", "__init__.py",
            "--time", "Ref::time"
        ]

        batch_task = aws_sfn_tasks.BatchSubmitJob(
            scope=self,
            id=f"batch_job_{stack_env}",
            job_definition=batch_job_definition,
            job_name=f"batch_job_{stack_env}_today",
            job_queue=batch_job_queue,
            container_overrides=aws_sfn_tasks.BatchContainerOverrides(
                command=command_overrides
            ),
            payload=aws_sfn.TaskInput.from_object(
                {
                    "time.$": "$.time"
                }
            )
        )

        # 今回は1ステップしかないので、単純ですが複数のステップをつなげたい場合は
        # batch_task.next(aws_sfn_tasks.JOB).next(aws_sfn_tasks.JOB)
        # のようにチェインメソッドで渡せます。
        definition = batch_task

        sfn_daily_process = aws_sfn.StateMachine(
            scope=self,
            id=f"YourProjectSFn-{stack_env}",
            definition=definition
        )

        # ================ #
        # CloudWatch Event #
        # ================ #

        # Run every day at 21:30 JST
        # See https://docs.aws.amazon.com/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html
        events_daily_process = aws_events.Rule(
            scope=self,
            id=f"DailySFnProcess-{stack_env}",
            schedule=aws_events.Schedule.cron(
                minute=31,
                hour=12,
                month='*',
                day="*",
                year='*'),
        )
        events_daily_process.add_target(aws_events_targets.SfnStateMachine(sfn_daily_process))

        # ここまで def __init__(...):

2.4.6 app.pyの実装(main関数の実装)

最後に、CDKを実行する処理を書いたら、完了です。

app.py
# ここに def __init__(...):


def main():
    app = core.App()
    BatchEnvironment(app, "your-project", "feature")
    BatchEnvironment(app, "your-project", "dev")
    BatchEnvironment(app, "your-project", "prod")
    app.synth()


if __name__ == "__main__":
    main()

2.5 デプロイ

上記のスクリプトが完成後に、以下のコマンドで正しくCDKの設定ができているか確認の上、デプロイしましょう。
Batchの環境を0から作成する場合でも10分程度で完了します。

# 定義の確認
$ cdk synth

Successfully synthesized to {path_your_project}/cdk.out
Supply a stack id (your-project-dev, your-project-feature, your-project-prod) to display its template.

# デプロイできる環境の確認
$ cdk ls

your-project-dev
your-project-feature
your-project-prod

$ cdk deploy your-project-feature

...deploying...

2.5.1 環境が正しく作られたか確認する

デプロイが完了したら、コンソールから作成したStepFunctionsを選択し、「実行の開始」ボタンを押します。

スクリーンショット 2020-09-27 23.38.58.png

timeの引数だけ入れてあげて、

{
    "time": "2020-09-27T12:31:00Z"
}

スクリーンショット 2020-09-27 23.45.12.png

正しく動いたら完了です。
また、CloudWatch Logsへも想定した通りに動いているか確認しましょう。

3. おわりに

CDKは、環境の構築と削除がコマンドですぐにできるのでめっちゃ好きです!

あと、コンソールから作成するよりも、プログラムのパラメータで求められているものがわかるので、
知らないサービスでも、どんなパラメータが必須かがわかりやすくて良いなと思いました!

(いつか、GitHubのリポジトリにて上記のソースをまとめたものを展開します・・・!)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む