AWS Fargate BlueGreenDeployment

はじめに

AWS FargateはContainerインスタンスの管理をAWSにお任せすることができるサービスです。

現状、ECS(LaunchType EC2)を使っているのですが、JenkinsからECSにBlueGreenDeployするときにecs-deployを使っています。
ecs-deployはaws cliとjqには依存していますがshellだけで書かれてるので持ち運びが便利なんですね。

ecs-deployはFargateに対応していないので対応させてみました。

https://github.com/uzresk/ecs-deploy.git

使い方

1. aws cliはFargateに対応しているバージョンをお使いください。

ちなみに私の環境はこちら

aws-cli/1.14.7 Python/2.7.12 Linux/4.9.62-21.56.amzn1.x86_64 botocore/1.8.11

2. コマンドはecs-deployと全く同じです

./ecs-deploy -c [cluster-name] -n [service-name] -i [registry-url]:[tag] -t 300 -r us-east-1

デフォルトのタイムアウトは90秒なのですが、終わらないことが何回かあったので少し長めにしておくのがおススメです。

実行結果

Using image name: xxxx.dkr.ecr.ap-northeast-1.amazonaws.com/xxxx:0.0.1-SNAPSHOT
Current task definition: arn:aws:ecs:us-east-1:xxxx:task-definition/xxxx:25
Current requires compatibilities FARGATE
New task definition: arn:aws:ecs:us-east-1:xxxx:task-definition/xxxx:26
Service updated successfully, new task definition running.
Waiting for service deployment to complete...
Service deployment successful.

変更点

Fargateが追加されたことによりrequiresCompatibilitiesの指定を引き継ぐようにしたのと、
cpu, memoryの設定も合わせて引き継ぐようにしました。
LaunchTypeがEC2の場合はcpu,memoryは設定されません。

[root@ip-10-0-0-100 ecs-deploy]# git diff
diff --git a/ecs-deploy b/ecs-deploy
index 637e793..8ad1cb1 100755
--- a/ecs-deploy
+++ b/ecs-deploy
@@ -261,11 +261,17 @@ function createNewTaskDefJson() {
     fi

     # Default JQ filter for new task definition
-    NEW_DEF_JQ_FILTER="family: .family, volumes: .volumes, containerDefinitions: .containerDefinitions"
+    NEW_DEF_JQ_FILTER="family: .family, volumes: .volumes, containerDefinitions: .containerDefinitions, requiresCompatibilities: .requiresCompatibilities"

     # Some options in task definition should only be included in new definition if present in
     # current definition. If found in current definition, append to JQ filter.
-    CONDITIONAL_OPTIONS=(networkMode taskRoleArn placementConstraints)
+    LAUNCH_TYPE=$(echo "$TASK_DEFINITION" | jq -r '.taskDefinition.requiresCompatibilities[0]')
+    echo "Current requires compatibilities $LAUNCH_TYPE"
+    if [ $LAUNCH_TYPE == FARGATE ]; then
+      CONDITIONAL_OPTIONS=(networkMode taskRoleArn executionRoleArn placementConstraints memory cpu)
+    else
+      CONDITIONAL_OPTIONS=(networkMode taskRoleArn executionRoleArn placementConstraints)
+    fi
     for i in "${CONDITIONAL_OPTIONS[@]}"; do
       re=".*${i}.*"
       if [[ "$DEF" =~ $re ]]; then

おわりに

もう少し動作確認したらプルリクエスト送ろうと思いますが、だいぶメンテされていないようなので多分マージされない気がします。。。

続きを読む

Cloud9 on Fargate など模索の経過報告

こんなの書いていきます

  • 利用例
  • 小ネタ

利用例

この辺に使えないかと模索しています

  • ECS のあふれたタスク処理
  • 踏み台サーバ
  • Cloud9 リモートサーバ

ECS のあふれたタスク処理

aws ecs describe-clusters--include "STATISTICS" を付与すると返ってくる pendingEC2TasksCount が 1 以上であれば run-task--launch-type FARGATE を指定。
とはいえスケーリングの設定やアプリケーションの作り次第では必ずしもこの負荷の逃がし方が適切でもなく、目下思案中。

踏み台サーバ

多段 SSH するときだけ起動する EC2 はありませんか?
Fargate にしてしまいましょう。

これくらいあれば最低限動きそうです。
https://github.com/pottava/fargate-shell/tree/master/serverless-bastion/docker

  • sshd に -d をつけて起動 することで、セッションが切れたら Fargate も落ちる
  • パスワード、または公開鍵認証(鍵は S3 を経由して配布)
  • sudo させるかどうかを環境変数 ENABLE_SUDO で制御

Docker イメージとして踏み台を管理できれば、これをベースに作業内容をログに残したり、渡す IAM タスクロールで AWS-CLI の利用できるコマンドを制限したりもある程度自由にカスタマイズできそうです。

Cloud9 リモートサーバ

Fargate で Cloud9 のリモートサーバを管理すれば、Docker イメージで作業者の環境を管理できそうです。ベースイメージはこんな感じ。
https://github.com/pottava/fargate-shell/blob/master/serverless-cloud9/docker/Dockerfile

  • AmazonLinux に必要なミドルウェアを入れたもの
  • Cloud9 からの SSH 接続に必要な鍵は S3 を経由して連携

その上で、開発に必要なミドルウェアを載せて、Cloud9 のリモートサーバに指定すれば IDE が起動します。例えば go v1.9.2 ならこんな感じ。
https://github.com/pottava/fargate-shell/tree/master/samples/cloud9-go1.9

ただし・・

  • docker in docker できないため、Cloud9 の
    IDE に c9.ide.lambda.docker はインストールできない
  • 作業が終了したら Fargate を明示的に停止する必要がある

のが惜しい感じになりました。AWS さんのネイティブ対応が待たれます。それにしても Fargate、ハンズオンや Jupyter notebook を配るといったことにも応用できそうです。

小ネタ

(以下 2017/12/12 時点のものであり、また仕様やドキュメントとして記載がないものも取り上げているため、機能追加や特にアナウンスなく変更が入る可能性も十分にありえます)

渡ってくる環境変数

  • AWS_DEFAULT_REGION
  • AWS_REGION
  • AWS_CONTAINER_CREDENTIALS_RELATIVE_URI

最後の変数でタスクに割り当てた IAM ロールを確認できる ものの
コンテナ内であればそのロールは Assume された状態なので
例えば以下のコマンドも同等の情報を返してくれます。普通。
$ aws sts get-caller-identity

渡せない環境変数

EC2 ホストがないので ECS Agent にオプションが渡せない。例えば ECS_ENABLE_CONTAINER_METADATAtrue にしたいけどできない。ECS_CONTAINER_METADATA_FILE が渡ってくると地味に便利なんですが・・

タスク定義の制約

とある理由で docker in docker がしたかったのですが、おそらくセキュリティ上の理由から ECS では設定できる以下の項目が使えません。まあ、はい。

  • linuxParameters/capabilities
  • privileged

awsvpc の制約

Fargate の注意点というわけではないものの、Fargate である以上 awsvpc が避けられないので。
ECS ではコンテナの定義として containerPorthostPort を別にすることができましたが、awsvpc ではそれが許されません。異なったポートを定義すると register-task-definition で弾かれます。

An error occurred (ClientException) when calling the RegisterTaskDefinition operation: When networkMode=awsvpc, the host ports and container ports in port mappings must match.

ENI の上限

起動するコンテナごとに一つ消費されていくので、初期リージョン上限である 350 個が一つのハードルでしょうか。Lambda ほどスケールしてくれませんが、まあ、ユースケース違うしね。ところでいつか awsvpc でない起動方法は追加されるんでしょうか。されない気もしますね・・

続きを読む

AnsibleでAWS環境(RDS)を自動構築する

はじめに

AWSの無料枠範囲で遊んでいます。

無料枠を超えないようにするため、作っては削除。作っては削除。をコンソールでやるのがめんどくさくなりました。

そのため、AnsibleでAWSの環境構築を自動構築できるようにしよう!ということで、
Ansible Playbookのサンプルを作成してます。

この記事で作成したplaybookはgithubで公開しています。

https://github.com/rednes/ansible-aws-sample
(AWS02フォルダ)

AWSの何を作るか

以下の内容をAnsibleで自動構築できるようにします。

  • VPCの作成
  • Internet Gatewayの作成
  • サブネットの作成
  • ルートテーブルの作成
  • セキュリティグループの作成
  • EC2インスタンスの作成
  • サブネットグループの作成
  • RDSの作成
  • EC2インスタンスにWordPress環境の構築

前提

  • ansible, botoをインストールすること
  • AWSのサーバー構築に使用するIAMユーザが作成されていること
  • AWSマネジメントコンソールでキーペア登録していること

AWS構成図

今回Ansibleで構築するAWSの構成図はこんな感じです。

AWS構成図

ディレクトリ構成

├── ansible.cfg
├── group_vars
│   └── all.yml
├── host_vars
│   └── localhost.yml
├── hosts
│   ├── ec2.ini
│   └── ec2.py
├── roles
│   ├── ec2
│   │   └── tasks
│   │       ├── ec2.yml
│   │       ├── main.yml
│   │       ├── security_group.yml
│   │       └── vpc.yml
│   ├── rds
│   │   └── tasks
│   │       ├── main.yml
│   │       └── rds.yml
│   ├── wordpress
│   │   ├── defaults
│   │   │   └── main.yml
│   │   └── tasks
│   │       └── main.yml
│   └── yum
│       ├── defaults
│       │   └── main.yml
│       └── tasks
│           └── main.yml
└── site.yml

Playbookの解説

AnsibleでAWS環境(RDS以外)を構築する内容については、過去の記事を参考にしてください。
今回はRDSの構築についてだけ説明します。

RDSの環境はrdsのroleで作成しています。

roles/rds/tasks/main.yml
---
- include_tasks: rds.yml

main.ymlでは単純にrds.ymlをインクルードしているだけです。
rds.ymlでRDSインスタンスを作成しています。

サブネットとセキュリティグループはec2のroleであわせて作成しています。

1. 異なるAZでサブネットを二つ作成

ec2_vpc_subnetモジュールでサブネットを構築します。
サブネットではVPC, Availability Zone, CIDRを指定します。

RDSでは単独のAZでしか使用しない場合でも、複数のAZにまたがったサブネットグループを作成する必要があるため、
今回は使用していませんがわざわざ使用しないAZにサブネットを作成しています。

サブネットグループ作成時に使用するサブネットを識別するため、タグに「route: private」を設定しています。

roles/ec2/tasks/vpc.ymlの一部
- name: subnet作成
  ec2_vpc_subnet:
    region: "{{ aws.common.region }}"
    state: present
    vpc_id: "{{ vpc_net.vpc.id }}"
    az: "{{ aws.common.region }}{{ item.value.zone }}"
    cidr: "{{ item.value.cidr }}"
    map_public: "{{ item.value.map_public|default(True) }}"
    tags: "{{ item.value.tags }}"
  with_dict: "{{ aws.vpc.subnet }}"
  register: vpc_subnet
host_vars/localhost.ymlの一部
aws:
  common:
    region: ap-northeast-1
  vpc:
    subnet:
      subnet1:
        tags:
          Name: public-a
          route: public
        cidr: 10.0.1.0/24
        zone: a
      subnet2:
        tags:
          Name: private-a
          route: private
        cidr: 10.0.2.0/24
        map_public: False
        zone: a
      subnet3:
        tags:
          Name: private-c
          route: private
        cidr: 10.0.3.0/24
        map_public: False
        zone: c

2. セキュリティグループを作成

ec2_groupモジュールでセキュリティグループを構築します。
セキュリティグループでは主にVPCとインバウンドルールを指定します。

AnsibleDBという名称のセキュリティグループを作成して、
EC2からのみ接続できるように設定しています。

roles/ec2/tasks/security_group.ymlの一部
- name: security group作成
  ec2_group:
    name: "{{ item.value.name }}"
    description: "{{ item.value.description }}"
    tags:
      Name: "{{ item.value.name }}"
      vpc_id: "{{ vpc_net_fact.vpcs[0].id }}"
    region: "{{ aws.common.region }}"
    purge_rules: "{{ item.value.purge_rules|default(False) }}"
    rules: "{{ item.value.rules }}"
  with_dict: "{{ aws.vpc.security_group }}"
  register: security_group
host_vars/localhost.ymlの一部
aws:
  common:
    region: ap-northeast-1
  vpc:
    security_group:
      security_group1:
        name: AnsibleWeb
        description: EC2 group
        rules:
          - proto: tcp
            ports:
              - 22
            cidr_ip: 0.0.0.0/0
          - proto: tcp
            ports:
              - 80
              - 443
            cidr_ip: 0.0.0.0/0
      security_group2:
        name: AnsibleDB
        description: EC2 group
        rules:
          - proto: tcp
            ports:
              - 3306
            cidr_ip: 10.0.1.0/24

3. サブネットグループを作成

rds_subnet_groupモジュールでサブネットグループを構築します。
サブネットグループでは主にsubnet idを指定します。

ec2_vpc_subnet_factsモジュールでタグが「route: private」である全てのサブネットを取得して、
一つのサブネットグループを作成しています。

roles/rds/tasks/rds.ymlの一部
- name: subnet取得
  ec2_vpc_subnet_facts:
    region: "{{ aws.common.region }}"
    filters:
      "tag:route": private
  register: ec2_subnet_facts
  check_mode: no

- name: rds-subnet-group作成
  rds_subnet_group:
    name: "{{ aws.vpc.rds.subnet_group.name }}"
    region: "{{ aws.common.region }}"
    state: present
    description: "{{ aws.vpc.rds.subnet_group.description }}"
    subnets: "{{ ec2_subnet_facts.subnets | map(attribute='id') | list }}"
  register: rds_subnet_group
host_vars/localhost.ymlの一部
aws:
  common:
    region: ap-northeast-1
  vpc:
    rds:
      subnet_group:
        name: wp-dbsubnet
        description: WordPress DB Subnet

4. RDSを作成

rdsモジュールでRDSインスタンスを構築します。
セキュリティグループはec2_group_factsモジュールを利用して、
名称からIDを引っ張ってきて定義しています。

roles/rds/tasks/rds.ymlの一部
- name: RDS作成
  rds:
    command: create
    instance_name: "{{ aws.vpc.rds.db_name }}"
    username: "{{ aws.vpc.rds.db_user }}"
    password: "{{ aws.vpc.rds.db_password }}"
    db_name: wordpress
    region: "{{ aws.common.region }}"
    subnet: "{{ aws.vpc.rds.subnet_group.name }}"
    vpc_security_groups: "{{ ec2_group_facts.security_groups[0].group_id }}"
    db_engine: "{{ aws.vpc.rds.db_engine }}"
    engine_version: "{{ aws.vpc.rds.engine_version }}"
    license_model: "{{ aws.vpc.rds.license_model }}"
    instance_type: "{{ aws.vpc.rds.instance }}"
    size: "{{ aws.vpc.rds.size }}"
    tags:
      Name: "{{ aws.vpc.rds.db_name }}"
  register: rds
host_vars/localhost.ymlの一部
aws:
  common:
    region: ap-northeast-1
  vpc:
    rds:
      db_name: wordpressdb
      db_user: root
      db_password: password
      db_engine: MySQL
      engine_version: 5.6.37
      license_model: general-public-license
      instance: db.t2.micro
      size: 20

注意点

RDSのエンドポイントがRDSを作成するまでわからないため、まず最初にRDSインスタンスまで作成した後に
RDSのエンドポイントをgroup_vars/all.ymlに追記する必要があります。

group_vars/all.ymlの一部
---
my_vars:
  rds:
    endpoint: XXXX # RDSで作成したDBのエンドポイントを入力

RDS作成が完了したら以下の様にawscli等を使用してエンドポイントを取得し、
上述のall.ymlにエンドポイントを記載するとEC2からRDSのmysqlに接続してWordPress環境を構築できます。

$ aws rds describe-db-instances --query="DBInstances[].Endpoint"

続きを読む

動画を探して自動ツイートしてくれるPython製botをAWSに載せてみた(後編)

TL;DR

  • YouTubeから動画を拾ってTweetするbotをPythonで開発し、AWS Lambdaに載せてみました
  • 全2記事です。後編のこちらでは、主にAWS Lambdaでのデプロイ・運用にフォーカスします
    • Pythonプログラムのパッケージングとアップロードに際しハマった知見を共有します
    • AWS CloudWatchと連携し、指定時間での自動ツイートを実現します
  • 前編はこちらです

AWS Lambda

AWS Lambdaとは

AWS Lambda はサーバーをプロビジョニングしたり管理しなくてもコードを実行できるコンピューティングサービスです。…コードが実行中でなければ料金はかかりません。

「一定の時刻に起動してツイート」さえしてくれればいいようなbotを動かすには、常時起動のサーバは必須ではありません。Lambdaのようなプラットフォームは今回のユースケースにうってつけと言えます。

関数をアップロードしておき、任意のイベントによって関数をトリガーするのがLambdaの基本的な使い方です。実際にやってみます。

ハマる、ハマる

が、実際には色々困りました…。順番に知見を共有します。

パッケージング

デプロイパッケージの作成 (Python)

Lambda 関数を作成するには、最初に Lambda 関数デプロイパッケージ (コードと依存関係で構成される .zip ファイル) を作成します。

ということで、プロジェクトのディレクトリに依存関係をインストールし、ZIP化する必要があります。pipのオプションでライブラリのインストール先は指定できるので、プロジェクトディレクトリに移動して…

$ pip3 install -r requirements.txt -t ./lib

ところが、エラーで失敗。

DistutilsOptionError: must supply either home or prefix/exec-prefix — not both

HomeBrewでPython導入しているとpip3 install -tが失敗する

らしいです。StackOverflowに同様の問題がありました。

ただ、回避の手段も回答されてます。ホームディレクトリに.pydistutils.cfgという名前のファイルを作って、以下の設定(というか、空のprefixを指定するハック)を書けば通るようになります。

pydistutils.cfg
[install]
prefix=

パッケージングの自動化について

pip installができるようになったのはいいとしても、増えた依存モジュールを再インストールして、もう一回.zipの中にモジュールを入れて…とか毎回手作業でやるのは辛いです。パッケージ管理ツールを導入して自動化できないかなあ、と調べたのですが色々動きが激しいようで。

ライブラリの配布について | Python Snippets

この辺りはベスプラを知りたいところではあります。追うのが大変そうだったのと、今回は大した規模でもないのでシェルスクリプトでなんとかすることにしました。

package_lambda.sh
#!/usr/bin/env bash

rm lambda.zip

cd src
pip3 install -r requirements.txt -t ./lib
zip ../lambda.zip *.py
cd lib
zip -r ../../lambda.zip *

src配下には自分で書いたPythonスクリプト群を配置しています。src/lib配下に依存モジュールをインストールし、それぞれ順番にZIPの直下に詰めてます。結論としてこれで大丈夫だったので先に進みます。

ハンドラ関数の定義

AWS Lambdaのコンソールに移ります。関数を作成、ランタイムには”Python3.6″を選びます。コードエントリで「.ZIPファイルをアップロード」を選び、作ったZipをアップしましょう。

スクリーンショット 2017-12-10 22.55.08.png

さて、次は画面右側の「ハンドラ」で処理の起点となる関数の名前を指定します。公式の説明では、

関数の filename.handler-method 値。たとえば、「main.handler」は、main.py で定義されたハンドラーメソッドを呼び出します。

すなわち[ファイル名].[関数名]とすれば良いので、前回作った感じだとmain.mainかなー、とか指定してテスト実行すると…

スクリーンショット 2017-12-10 23.08.36.png

落ちます。

ハンドラ関数の引数の数が間違っていると落ちる

"errorMessage": "main() takes 0 positional arguments but 2 were given"

Lambda 関数ハンドラー (Python)

まあドキュメントを読めという話でお恥ずかしいのですが、引数が合ってないわけですね。イベントハンドラであるところのLambda関数はeventとcontextを受けるのが基本ですから、その形に沿ったハンドラ定義が必要です。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from tube_crawler import TubeCrawler
from tweeter import Tweeter
import config

def main():
    t = TubeCrawler()
    movies = t.movies_from_query("Hybrid Rudiments")
    chosen = t.choose(movies)

    tw = Tweeter()
    tw.reply(config.REPLY_TO, chosen)
+   return chosen

+def lambda_handler(event, context):
+   result = main()
+   return { 'tweetedURL': result }

if __name__ == '__main__':
    main()

あとは、コンソールでmain.lambda_handlerをハンドラに指定すればOKです。せっかくなので実際にツイートされた動画のURLを呼び出し元に返す仕様にしてみました。

定期実行

デプロイはできたので、次は定時実行の仕組みを作ります。

スクリーンショット 2017-12-11 22.05.53.png

コンソール画面の「トリガーの追加」から「CloudWatch Events」を選びます。トリガーのルールにcron式を選べば、今回やりたいことは実現できますね。

Rate または Cron を使用したスケジュール式

  • 日または週日の値は疑問符である必要がある
  • UTCしか使用できない(ので、日本時間に合わせてずらす)

あたりが注意点でしょうか。今回は平日の定時につぶやいて欲しいので、こんな式にしてみます。

cron(15 10 ? * MON-FRI *)

運用してみて

運用して1週間ほど様子を見てみましたが、無料利用の範囲内でおおむね問題なく動いております。
今度はTedみたいな勉強系の動画やらブログ記事やら拾わせても面白いかもしれませんね。

よかったこと

一気通貫して人が使えるサービスの形まで持って行くと、否応無しに広く技術をさらうことになります。インフラよりに苦手意識があったので半ば無理やりにでも触るのはいい経験になりました。

反省

  • YouTubeAPIとか使えばもっと楽に実装できたんじゃないか疑惑
  • パッケージングとAWS周りでハマりすぎた。アップロードが絡むあたりからはTry&Errorより前にドキュメントを読もう

お読みいただきありがとうございました。

リンク

続きを読む

動画を探して自動ツイートしてくれるPython製botをAWSに載せてみた(前編)

TL;DR

  • YouTubeから動画を拾ってTweetするbotをPythonで開発し、AWS Lambdaに載せてみました
  • 全2記事です。前編のこちらでは、主にPythonでの開発周りのトピックにフォーカスします
    • TwitterAPIを使ってプログラムからツイートしてみます
    • YouTubeのページを構文解析し、文字列操作を使って動画URLを抽出してみます

動機

新しい職場にて初めてAWSを触ることになったので、これを機にと個人アカウントを取ってみました。チュートリアルだけというのももったいないので、何か自分のためのサービスを作って載せると面白そうです。

で、Twitterのbot開発にはもともと興味があったので、これも前から興味を持ちつつ触ってなかったPythonでbotを作り、lambdaを使って運用してみようと思い立ちました。AWS lambdaは2017年4月からPython3系を扱えるようになったので、心置き無く最新バージョンで書けそうだなー、というのも狙いです。

ユーザーストーリー

毎日の退勤をもう少し楽しみにするために、定時になると自分が興味ありそうなYouTube動画をbotが勝手に検索して、自分のTwitterアカウントに届けてくれるようにしたい。
スクリーンショット 2017-12-06 23.30.33.png

前提

  • 開発にはMacを使用します
  • Pythonは3.6系を使用します
  • pyenvもvirtualenvも使用しません。議論はあろうかと思いますが、個人開発なので。。
  • で、開発環境構築はこちらの記事等を参照しました
  • bot化したいTwitterアカウントはあらかじめ用意してあるものとします

TwitterAPIを使ってプログラムに呟かせる

アクセスキーの取得

bot化したいアカウントでTwitter Application Managementにログインすると、アプリケーションの作成とConsumer Key、及びAccess Tokenの取得ができます。

なお、Appの作成にはTwitterアカウントが電話番号認証済みである必要があります。認証済みでないと怒られるので、エラーメッセージ中のリンクからさらっと済ませておきましょう。

  • Consumer Key
  • Consumer Key Secret
  • Access Token
  • Access Token Secret

以上の4パラメータがあればプログラムからのツイートができます。コピーしてこんな感じのファイルを作っておきましょう。

config.py
CONSUMER_KEY        = "xxxxxxxxxxxxxxxxx"
CONSUMER_SECRET     = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
ACCESS_TOKEN        = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
ACCESS_TOKEN_SECRET = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"

複数の外部ユーザーからアクセスがあるようなアプリケーションの場合(=「このアプリケーションとの連携を許可しますか?」など出るやつ)はそれぞれの役割についてもう少し説明が必要ですが、今回はある程度一緒くたに考えてしまっても実装に支障ありません。

PythonでOAuth認証

ライブラリの導入と管理

Pythonのライブラリは、パッケージ管理ツールであるpipでインストールできます。仮想環境がない場合、オプション無しで勝手にglobalに入るのがうーん、という感じですがまあそれは置いておいて。

PythonでHttp通信を行うライブラリとしては、requestsがポピュラーなようです。また、今回はTwitterAPIを使うための認証が必要なので、OAuth認証を扱えるライブラリも必須です。ここはrequestsと同じところが公開しているrequests_oauthlibを使用しました。

pip3 install requests requests_oauthlib

さて、インストールはできましたが、今度は開発するプロジェクトがこれらのライブラリに依存していることを表明しておくのがマナーです。js界隈で言うところのpackage.jsonですね。

Pythonでは依存関係を記したrequirements.txtなどを作っておくケースが多いようです。

requirements.txt
requests==2.18.4
requests-oauthlib==0.8.0

ちなみに、pip3 freeze > requirements.txtでインストールされた依存関係をrequirements.txtに吐き出せます。

逆に.txtファイルを元に一括インストールする場合は、-rオプションを用いてpip3 install -r requirements.txtなどと書けます。結構便利です。

つぶやいてみる

first_tweet.py
from requests_oauthlib import OAuth1Session
import config, json

twAuth = OAuth1Session(
  config.CONSUMER_KEY,
  config.CONSUMER_SECRET,
  config.ACCESS_TOKEN,
  config.ACCESS_TOKEN_SECRET)
apiURL = "https://api.twitter.com/1.1/statuses/update.json"
params = { "status": "プログラムにツイートさせてみるテスト" }

res = twAuth.post(apiURL, params = params)
print(json.loads(res.text))

先ほど作ったconfig.pyimportして、これだけ。思ったよりだいぶ手軽です。Twitterにアクセスして実際にツイートされたことを確認しましょう!

また、せっかくなのでレスポンスをjsonライブラリでロードして吐き出してみます。

{'created_at': 'Wed Dec 06 14:00:00 +0000 2017', 'id': 9384076800000000, 'id_str': '9384076800000000', 'text': 'プログラム
にツイートさせてみるテスト', 'truncated': False, 

...(中略)...

'retweeted': False, 'lang': 'ja'}

思ったよりいろんな属性があることがわかりますね。深掘りは公式のリファレンスにて。

YouTubeから動画のURLを拾ってくる

続いて、YouTubeから動画を探してくるパートです。

Webクローリング

この分野では、「クローリング」や「スクレイピング」と言った言葉が有名です。

クローリングとスクレイピング

クローリングはウェブサイトからHTMLや任意の情報を取得する技術・行為で、 スクレイピングは取得したHTMLから任意の情報を抽出する技術・行為のことです。

たとえば、あるブログの特徴を分析したい場合を考えてみましょう。
この場合、作業の流れは

  1. そのブログサイトをクローリングする。
  2. クローリングしたHTMLからタイトルや記事の本文をスクレイピングする。
  3. スクレイピングしたタイトルや記事の本文をテキスト解析する。

というようになります。

今回は、YouTubeをクローリングし、その中から動画のURLをスクレイピングすることになりますね。

Webページのクローリングとスクレイピングを行う際は、それがどんな目的のものであれ、HTMLを構文解析することが必須となります。Pythonでは、これを強力に支援するBeautifulSoupと言うライブラリがあります。執筆時点で最新のbeautifulsoup4を導入してみます。

pip3 install beautifulsoup4

早速使ってみましょう。Qiitaのトップページから<a>タグを探し、その中に含まれるhref属性の値を取得してみます。

crawling.py
import requests
from bs4 import BeautifulSoup

URL = "https://qiita.com/"
resp = requests.get(URL)

soup = BeautifulSoup(resp.text)

# aタグの取得
a_tags = soup.find_all("a", href=True)
for a in a_tags:
    print(a["href"])

結果

/about
https://qiita.com/sessions/forgot_password
https://oauth.qiita.com/auth/github?callback_action=login_or_signup
https://oauth.qiita.com/auth/twitter?callback_action=login_or_signup

・・・(中略)

https://qiita.com/api/v2/docs
https://teams.qiita.com/
http://kobito.qiita.com

いい感じです!

HTMLパーサーについて

さて、先のコードを実際に試すと、HTMLパーサーが明示されていないために警告が出ます。これは実際の解析時に使われるパーサーが実行時の環境に依存するためです。異なる環境下で同じ振る舞いを期待するには、使用するHTMLパーサーを明示してあげる必要があります。

デフォルトではhtml.parserが使われますが、lxmlかhtml5libを導入してこちらを明示してあげるのが無難なようです。このあたりの情報は下記の記事をだいぶ参考にさせていただきました。パーサーの選択だけでなくスクレイピング全般の情報が非常によくまとまっているエントリなので、オススメです。

PythonでWebスクレイピングする時の知見をまとめておく – Stimulator

パーサの良し悪しを考えるとlxmlでチャレンジしてダメならhtml5libを試すのが良さそう。

今回はこの1文に愚直に従ってみます。事前にpip3 install lxml html5libも忘れずに。


import requests
from bs4 import BeautifulSoup

URL = "https://qiita.com/"
resp = requests.get(URL)

+try:
+  soup = BeautifulSoup(resp.text, "lxml")
+except:
+  soup = BeautifulSoup(resp.text, "html5lib")
-soup = BeautifulSoup(resp.text)

# ...以下は先ほどと同様

Crawlerクラスを作ってみる

すでにPythonでオブジェクト指向な書き方を経験している方はこの辺りを飛ばしていただいて構いません。せっかくHTMLを解析してくれるコードができたので、クラスとして書き換えてみます。

crawler.py
import requests
from bs4 import BeautifulSoup

class Crawler:
    def hrefs_from(self, URL):
        a_tags = self.soup_from(URL).find_all("a", href=True)
        return set(map(lambda a:a["href"], a_tags))

    def soup_from(self, URL):
        res_text = requests.get(URL).text
        try:
            return BeautifulSoup(res_text, "lxml")
        except:
            return BeautifulSoup(res_text, "html5lib")

個人的にはインスタンスメソッドの第1引数が常にselfでなければならないのは書く量が増えるので少しもどかしいですね。ハマりポイントにもなりかねない…。

ちなみに、ここではラムダ式を使用し、hrefs_fromメソッドの戻り値の型をsetにしてみました。これは、今回のユースケースを鑑みてリンク先URLの重複を排除した方が便利と判断したためです。出現頻度など解析したい場合はまた改めて設計を考える必要があるでしょう。

継承と、YouTubeへのアクセス

YouTubeをクローリングするにあたって、「検索文字列を与えたら検索結果のページをクローリングし、動画を探してくる」などの機能があると便利そうです。先ほどのクラスを継承して、実装してみます。

tube_crawler.py
import random
import re
from crawler import Crawler

class TubeCrawler(Crawler):

    URLBase = "https://www.youtube.com"

    def hrefs_from_query(self, key_phrase):
        """
        検索文字列を与えると検索結果ページに含まれるhref属性の値を全て返す
        """
        return super().hrefs_from(self.URLBase + 
            "/results?search_query=" + key_phrase.replace(" ", "+"))



    def movies_from_query(self, key_phrase, max_count = 10):
        """
        検索文字列を与えると検索結果ページに含まれる動画のビデオIDを返す
        """
        return self.__select_movies(self.hrefs_from_query(key_phrase), max_count)



    def __select_movies(self, hrefs, max_count):
        """
        privateメソッド。href属性の値のsetからビデオIDのみを返す
        """
        filtered = [ re.sub( "^.*/watch?v=", "", re.sub( "&(list|index)=.*$", "", href )) 
            for href in hrefs if "/watch?v=" in href ]
        return filtered[:min(max_count, len(filtered))]



    def choose(self, movie_ids, prefix = "https://youtu.be/"):
        """
        渡した文字列のリスト(ビデオIDのリストを想定)から1つを選び、prefixをつけて返す
        """
        return prefix + random.choice(movie_ids)

文法的には継承とprivateメソッドの書き方あたりが新しい話題となります。この記事の主題ではないので特段の説明は省きます。

実際に試すとわかるのですが、検索結果のページにノイズとなるリンクが多いばかりか、再生リストへのリンクなど紛らわしいものも多く、その辺を適切に弾いていくのに手こずりました。おかげでfilter関数や正規表現に少し強くなれた気がします。

正規表現についてはこちらの記事をだいぶ参考にしました。

Pythonの正規表現の基本的な使い方

繋げてみる

準備が整ったので検索->ツイートの流れを試してみます。

main.py
#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from tube_crawler import TubeCrawler
from tweeter import Tweeter
import config

def main():
    t = TubeCrawler()
    movies = t.movies_from_query("Hybrid Rudiments")
    chosen = t.choose(movies)

    # ツイートする部分をクラス化したもの
    tw = Tweeter()
    tw.reply(config.REPLY_TO, chosen)

if __name__ == '__main__':
    main()

エントリーポイントとなる関数が必要かなー、と思ったので何気なく(そう、本当に何気なく。これで良いと思っていたんですLambdaを使うまでは…)main関数を作成。

直接./main.pyでも呼べるようにこの辺からShebangを記述し始めました。また、末尾はファイル名で直接実行した場合にmain()を呼ぶためのおまじない。Rubyにも似たやつがありますね。あとはターミナルから呼んで動作確認するだけです。

$ ./main.py

実行したところ問題なく動きそうだったので、次回はAWS Lambdaに載せていきます。それなりの尺となったのでこのページはここまでです。お読みいただきありがとうございました。

リンク

続きを読む

phpでlocalstackのs3にファイルをアップロード

AWS周りのものをローカルで開発する場合 localstack を使うと便利だというのを聞いたので試してみました。 github.com localstackは、GUIはあまり提供されていない用で、主にawscliコマンドを使います。 s3にファイルをアップロードしたい場合は、 –endpoint-url=http:… 続きを読む

中途入社のAWSエンジニアが、稼働中サービスの運用状況をキャッチアップするために意識すること

Classiアドベントカレンダー11日目です。
今回は、AWSエンジニアが稼働中のAWSの管理アカウントを渡されて、ビクビクしながらキャッチアップを行っていったときのメモになります。

1.TL;DR

AWSアカウントのログイン前に準備できることもあるし、AWSアカウントにログインしてわかることもあるし、サーバーにログインしてわかることもある。それぞれのレイヤーでどういったことを確認するかを意識しながらキャッチアップを進めていきましょう。

2.AWSアカウントログイン前の事前準備

pre_aws.png

サービスが稼働しているのであれば、AWSアカウントにログインせずとも、たくさんの情報をキャッチアップすることができます。1日目から何らかの大きなアウトプットを出さないと解雇するような会社は、(おそらく)存在しない筈です。まずは落ち着きましょう^^;

2-1.ドキュメント読み込み

サービスのインフラにAWSが使われることが多くなったからといって、入社前に経験したAWS運用フローがそのまま活かせる訳ではありません。まずは、前任者や運用中のドキュメント管理ツールの中から、今までどのような運用を行っていたかを確認します。
ドキュメントを見たときに意識する観点としては、

  • フロー型:時間による鮮度の劣化があるドキュメント
  • ストック型:システム仕様など、メンテナンスが求められるドキュメント

どちらの情報であるかを意識して読むことが重要です。
フロー型の情報は、障害などで一時的な対応用にメモっていることもあり、運用の中で解決済みのことがあります。そのため、ストック型のドキュメントを中心に見ることが素早いキャッチアップになると思います。
とはいえ、ドキュメントの全てがメンテナンスされている会社というのは稀だと思いますので、各種ドキュメントを見ながら、仮説程度に自分なりのシステム構成図などを書いてみましょう。
要件定義書や各種構成図の変更履歴、課題管理表、リスクコントロール表といったドキュメント類があるのであれば、目を通しておきましょう。

2-2.運用フローを観察する

サービス側のドキュメントについては、まだ文書化されてることが多いですが、運用系ツールに関しては、ドキュメント化されていないことがあります。今の開発スタイルであれば、何らかのチャットツール(Slack,ChatWork,HipChat)上で、

  • デプロイ
  • 各種の通知
  • 運用Bot

の運用といったことが行われていると思います。また、チャットだけでなく、メールでの運用フローも存在しているかもしれません。
こうした運用系ツールの存在は、今後自分がリファクタするときに、「必須要件ではないが、重宝している」ということで、「リファクタ後にも、あの機能を実装して欲しい」といった声が社内から上がると思います。
そのため、このような運用フローがどこで実装されているかを見極めることが重要になってきます。

2-3.インフラ部分のコード読み

「俺はフルスタックエンジニアだ!」という強い意思がある方は、この時点で稼働中のアプリ側のコードまで読み込んでいただければよいですが、まずは入社前に期待されたであろう、インフラまわりのコード化部分を把握しておきましょう。どのみち、いずれはメンテナンスを任されたり、質問されることが増えてきますので、自分のメンテナンスする部分を優先的に確認しておきましょう。
実サーバーの運用はAWSに任せているので、ここで意識しておくことは、

  • Infrastructure Orchestration Tools:Terraform, CloudFormationなど
  • Server Configuration Tools:Chef,Ansible,Itamaeなど

あたりのコードがgithubなどに保存されているからといって、メンテナンスされていない可能性もあります。
コードの設計方針などを確認するのは当然必要なのですが、コードの変更履歴が年単位で放置されていないかどうかも見ておきましょう。特に、AWS関連のコードについては、担当する人がアプリ側よりも少ないので、構築当初のコードのままなのか、運用されているコードなのかはPRなどで確認しておいた方がよいです。

3.AWSのアカウント内を調査する

aws_kansatsu.png

実際にAWSアカウントにログインしたり、APIで各種設定を確認していきます。Web系サービスであれば、TCP/IPモデルやC/Sモデルを意識しながら、下層レイヤー回りから調査していき、ネットワークがどうせ設定されているかを確認していきます。
おそらく、ここで多くの疑問(場合によっては、絶望)が生まれる段階です。「あれ?ドキュメントにこう記述されているけど、設定上は違うような…」という沼にハマることがあるでしょう。負けないでください、一人で抱え込まずに闇を共有できる仲間を見つけましょう。

3-1.外部システム連携の確認

関連するAWSサービス

VPC関連のサービスを中心に、自AWSアカウント以外の連携がないかの確認を行います。

関連しやすいAWSサービス例)

  • DirectConnect
  • NAT Gateway
  • Peering Connection
  • Customer Gateways
  • Virtual Private Gateways
  • VPN Connections
  • NetWorkACL
  • SecurityGroup
  • EIP

などに、何らかのインスタンスが稼働していて、productionやhonbanなどの文言がついたものがあれば、それはドキュメント上には存在しないが、サービス上何らかの理由で稼働しているものである可能性があります。
自社のサービスがAWSアカウント内だけで完結しているのであればよいのですが、誤ってここのインスタンスなどを削除すると、場合によってはシステム復旧できないぐらいの痛手になるので、慎重に確認していきましょう。
特に、SecurityGroupは、最近でこそInboundルールにDescriptionをつけられるようになりましたが、数年運用されているシステムには、何で利用しているIPアドレスなのかがわからないことがあるので、設定確認中に不明なIPアドレスを見つけたら社内で有識者に聞いてみましょう。

3-2.システム導線の確認

関連するAWSサービス

インスタンス障害があるとユーザー影響がありそうな、システム導線を追っていきます。

関連しやすいAWSサービス例)

  • ELB(CLB,ALB,NLB)
  • CloudFront
  • EC2
  • ElasticCache(redis,memcached)
  • RDS(Aurora,MySQL,PostgreSQL,SQLServer)
  • ElasticSearch
  • DynamoDB
  • S3

各種のインスタンスサイズが適切かを確認するのはもちろんですが、DB関連についてはバックアップ関連の設定がちゃんと行われているかどうかも確認しておきましょう。バックアップウィンドウの世代数やメンテナンスウィンドウの時間が営業時間内になっているとかは、結構ありがちな設定漏れケースになります。パラメータストアの設定については、本番で稼働している設定が正義なので、設計と違う場合は、社内で経緯を追ってみましょう。

3-3.運用導線の確認

関連するAWSサービス

直接のユーザー影響はないものの、バッチ系およびログインやログ連携など、システム運用で必要な運用導線を追っていきます。

関連しやすいAWSサービス例)

  • EC2
  • Lambda
  • ElasticSearch(& kibana)
  • IAM
  • CloudTrail
  • AWS Config
  • CloudWatch Logs
  • S3
  • Glacier

24224というポート開放を見れば、そのシステムはfluentd関連のフローがあるのはほぼ確定なので、ログの発生から可視化ツールおよびバックアップのフローまで追っていきましょう。また、バッチ系のEC2に関しては、最近のAWSだと、FargateやECS、Lambdaなどで定期バッチを行うことも可能なので、単一障害点をなくすことができないか、今後の計画のために、バッチ系が整理されているドキュメントなどを探してみましょう。

4.サーバー内の設定を確認する

server_chosa.png

最近だと、Server Configuration Toolsが大分普及していたり、コンテナ系の運用が発達してきているので、このあたりのキャッチアップ期間が少なくなるのかなと思います。とはいえ、SSH接続を頻繁に行うサーバーや起動時間が長いサーバーだと、コードの設定と異なる部分が出てきていることがあるかもしれません。
OSの設定やミドルウェアのバージョンなど、SSH接続すると確認した方がよいところは多々ありますが、Server Configuration Toolsの設定と異なっていたり、運用中のアラート設定などで差異がでやすそうな部分を以下に記載します。

4-1.各種メトリクス確認

メモリやプロセスの状況は、通常CloudWatchだけではわからないので、MackerelやZABBIXなどの監視ツールエージェントを入れているのであれば、各サーバーのメトリクスを確認しておきましょう。

4-2.稼働プロセスの確認

pstreeなどで、稼働しているプロセスを確認します。SSH接続が禁止されているのであれば、AWSのSSMエージェントなりで確認できないかを検討してみましょう。設計上のソフトウェアスタックに存在しないプロセスが常駐している場合は、何のエージェントが動いているかを追っておきましょう。

4-3.不要なファイルが出力されていないかの確認

ログレベルがデバッグのままだったり、ログファイルのローテートがなされていない場合があり、アラートは上がっていないけど、サーバー内のリソースを侵食しているときがあります。また、生成されるログファイルが小さすぎると、ディスクに余裕がありそうに見えても、inodeが先に枯渇するときもあります。lsofdf -iなどを可視化するなどして、サーバー内のディスク状況を確認しておきましょう。

4-4.同期処理と非同期処理のプロセス確認

同期処理のプロセスは意識しやすいので、監視対象に入っている可能性が高いです。ただ、非同期系プロセス(Rubyだとsidekiq,Pythonだとcelery,PHPだとphp-resqueなど)が監視対象に入っていないこともあるので、どのサーバーで非同期処理が行われているかを把握しておきましょう。

5.まとめ

AWSや他のパブリッククラウドが全盛になった今でも、3層アーキテクチャのシステム構成やOSI7階層などのレイヤーを意識しながらキャッチアップを行うと、システムを俯瞰しながらキャッチアップを進めることができるのではないかなと思います。とはいえ、前任者がコード化しきれなかった部分もある筈なので、そこは社内で過去経緯を知っている人に笑顔で質問をしてみましょう。技術が発達しても、人に蓄積されるノウハウはまだまだ多いので…
AWSエンジニアが転職する際などのご参考になれば。

続きを読む

Windows 10 ローカルに AWS S3 のクローン(minio)を手軽につくる

2017/12/10

Dockerのほうが動かなかったのでWSL側で動かしてみたらすぐにできたのでメモ。
(windowsでkitematic上で手軽に終えたかったのですが)

WSL: Windows Subsystem for Linux

(minioにたどり着くまでに、fakes3とs3ninjaをdockerで使ってみましたが、aws-sdkでput時にエラーが出たので乗り換えました)

minio のインストールと起動まで

以下からLinux向けをダウンロード
https://github.com/minio/minio

  • rootユーザーでやりましたが、rootである必要はないかも・・・
cd /root
wget https://dl.minio.io/server/minio/release/linux-amd64/minio

chmod +x minio
mkdir /minio_data

./minio server /root/minio_data

動作すると下記のような表示が出ます。

Created minio configuration file successfully at /root/.minio
Endpoint: ~省略~ http://127.0.0.1:9000
AccessKey: ~省略~
SecretKey: ~省略~

Browser Access: ~省略~ http://127.0.0.1:9000

Command-line Access: ~省略~ 

Object API (Amazon S3 compatible):
   Go:         https://docs.minio.io/docs/golang-client-quickstart-guide
   Java:       https://docs.minio.io/docs/java-client-quickstart-guide
   Python:     https://docs.minio.io/docs/python-client-quickstart-guide
   JavaScript: https://docs.minio.io/docs/javascript-client-quickstart-guide
   .NET:       https://docs.minio.io/docs/dotnet-client-quickstart-guide

Drive Capacity: 532 GiB Free, 910 GiB Total

minio の UI を使ってみる

  • ブラウザで http://127.0.0.1:9000 にアクセス

image.png

  • 上記のメッセージに出てきたアクセスキーとシークレットキーを入力してログイン

image.png

  • バケットを作る (入力してenter)

image.png

  • バケットを消す(消す機能はUIにない?)

仕方なく・・・ rm -rf で削除

# ll /root/minio_data/
合計 0
drwxrwxrwx 0 root root 4096 12月 11 01:17 ./
drwx------ 0 root root 4096 12月 11 00:50 ../
drwxrwxrwx 0 root root 4096 12月 11 00:57 .minio.sys/
drwxrwxrwx 0 root root 4096 12月 11 01:16 sample/
drwxrwxrwx 0 root root 4096 12月 11 01:17 sample2/
rm -rf /root/minio_data/sample2

ちゃんと消えました

spring boot から minio にアクセスしてみたときのポイント

接続情報としては下記でOKでした(SDKバージョンなどで書き方は少し違いがあるかもしれません)

エンドポイント:http://127.0.0.1:9000
アクセスキーとシークレットキーは上記のもの

ClientConfiguration clientConfig = new ClientConfiguration();
clientConfig.setProtocol(Protocol.HTTP); <- ここがポイント!

EndpointConfiguration endpointConfiguration = new EndpointConfiguration("http://127.0.0.1:9000", null); <- ここがポイント!

AWSCredentials credentials = new BasicAWSCredentials(ACCESS_KEY, SECRET_KEY); <- ここがポイント!
AWSStaticCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);

AmazonS3 client = AmazonS3ClientBuilder.standard()
                            .withCredentials(credentialsProvider)
                            .withClientConfiguration(clientConfig)
                            .withEndpointConfiguration(endpointConfiguration).build();

今日はここまで m(_ _)m

続きを読む

LambdaとCloudWatchでサーバレスなTwitterBot作ってみた

こんにちは。駆け出しインフラエンジニアの(@k4ri474)です。
この記事はVASILY Advent Calendar 2017 11日目の記事です。

Twitterでbotを作ろうと思った時、ざっくり二つ選択肢が頭に浮かびますよね?
巷に溢れているbot作成サービスに登録するか、
はたまた自作するか。

エンジニアの方は、とりあえずプログラミングしてみっか〜となることが多いような気がします。
Twitter APIに関してはもちろん公式ドキュメントがありますし、プログラムもググれば山ほど先人たちのものが見れるので、
なんちゃらキーやらなんちゃらトークンさえ取得できれば、肝心のツイートはチョチョイのジョイかと思います。

ただ、継続的に動かすbotとして考えるとベースのサーバはどうしても必要になるのでちと悩みますよね。
自分のサイトをホスティングしてメールサーバ動かしてついでにbotを回す、などといったような状況ならクラウドでサーバを立てとけばいいんですが、botだけでいい時には少々大げさです。

そこでAWS Lambda + Amazon CloudWatch Eventsを使ってサーバレスに、かつ継続的に実現します。

概要

1postするためのプログラムをpythonで書き、それをLambdaにセットします。
そしてLambdaをCloudWatch Eventsで定期的に発火させ、botの体をなすようにします。

実装(Lambda)

まず、こちらが普通のつぶやきプログラムです。

from requests_oauthlib import OAuth1Session

CK = 'CONSUMER_KEY'
CS = 'CONSUMER_SECRET'
AT = 'ACCESS_TOKEN'
AS = 'ACCESS_TOKEN_SECRET'

URL = 'https://api.twitter.com/1.1/statuses/update.json'

tweet = "Hello World"
session = OAuth1Session(CK, CS, AT, AS)

params = {"status": tweet }
session.post(URL, params = params)

Twitterに接続するにはOAuth認証が必要なのでrequests_oauthlib(https://github.com/requests/requests-oauthlib)
というPython用のOAuth認証ライブラリを利用しました。
Lambdaではライブラリとプログラムコードを一まとめにzip圧縮する必要があるので、プログラムと同じディレクトリにインストールしておきます。

% pip install requests requests_oauthlib -t ./

各種キーは苦労して取得したら、それらを各々セットしていただければあとはつぶやきをいじって実行するだけです。

さて、上のプログラムをLambda用に書き換えます。
Lambdaから関数を呼び出してもらうためには、ハンドラー関数を作成する必要があります。
パラメータとして eventcontext を取る関数で、Lambdaを呼び出したイベントデータやランタイム情報を内部で使えますが、今回はシンプルに使わない方向でいきます。

from requests_oauthlib import OAuth1Session

CK = 'CONSUMER_KEY'
CS = 'CONSUMER_SECRET'
AT = 'ACCESS_TOKEN'
AS = 'ACCESS_TOKEN_SECRET'

URL = 'https://api.twitter.com/1.1/statuses/update.json'

def my_handler(event, context):
    tweet = "Hello World"
    session = OAuth1Session(CK, CS, AT, AS)

    params = {"status": tweet }
    session.post(URL, params = params)

これで準備は完了です。
こちらのファイル群をzip化し、コマンドでアップロードしてみます。

% aws lambda create-function \
--region ap-northeast-1 \
--function-name "sample" \
--runtime "python3.6" \
--role "MY_ROLE" \
--handler "sample.my_handler" \
--zip-file "fileb://./sample.zip" \
--profile PROFILE

注意したいのはhandlerオプションです。構文は実行ファイル名.ハンドラー関数名ですので、作成したプログラム名とハンドラー関数名に合わせて適宜編集してください。

また、セットするroleにCloudWatchLogsの権限を与えておくと実行結果をログ出力できるようになるのでオススメです。
今回は定義済みのポリシー、arn:aws:iam::aws:policy/CloudWatchLogsFullAccessをアタッチしたロールを事前に作成していたので、そちらをセットしてみました。
ポリシーは以下の通りです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "logs:*"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}

以上でLambdaへのアップロードは完了です。

実装(CloudWatch Events)

さて、あとはこのLambda関数を定期実行するだけです。
まずはCloudWatch Eventsをトリガーにセットします。

スクリーンショット 2017-12-10 23.09.08.png

Lambdaのダッシュボードから関数を選択し、設定画面でトリガーを設定します。上図のようにCloudWatch Eventsをトリガーにセットしたら、下にスクロールしてトリガーの詳細を設定します。

スクリーンショット 2017-12-10 23.09.56.png

例としてはこんな感じでしょうか。
ルールタイプをスケジュール式に選択すると、おなじみのcron記述で定期実行の間隔をコントロールできます。
僕は試しにもう一方のrate式を使ってみることにしました。

このような感じで設定して保存すると無事トリガーとして機能します。
お疲れ様です。

成果物

このままのプログラムでは全く同じ内容をつぶやくばかりで面白くないので、
プログラムをちょちょっといじってimgurから猫画像のリンクを拾ってきてpostする感じのbotを作ってみました。

https://twitter.com/k4ri474/status/937562270476812290

自分の猫postで心が洗われます。
皆さんもサーバレスTwitter botでぜひ遊んでみてくださいb

続きを読む

Docker + Nginx + Let’s EncryptでHTTPS対応のプロキシサーバーを構築する

Docker上にNginxコンテナをプロキシサーバーとして構築し、Let’s EncryptでHTTPS対応しました。構築にあたって かなり苦戦した ので、そのノウハウを記事としてまとめました。

「Nginx」とは

Apacheなどの従来のWebサーバーは、クライアントの数が多くなるとサーバーがパンクする 「C10K問題(クライアント1万台問題)」 を抱えていました。「Nginx」はこの問題を解決するために誕生した、静的コンテンツを高速配信するWebサーバーです。2017年10月現在、そのシェアは Apacheとほぼ同等 となっています。

wpid-wss-share13.png

Webサーバー シェア
Micosoft IIS 49.44%
Apache 18.78%
Nginx 18.40%

「Let’s Encrypt」とは

「Let’s Encrypt」は すべてのWebサーバへの接続を暗号化する ことを目指し、SSL/TLSサーバ証明書を 無料 で発行する認証局(CA)です。シスコ、Akamai、電子フロンティア財団、モジラ財団などの大手企業・団体がスポンサーとして支援しています。


本稿が目指すシステム構成

本稿ではAmazon EC2、Dockerコンテナを使用して以下のようなシステムを構築することを目標とします。

DockerでNgixのプロキシサーバーを構築する.png

前提条件

  • 独自ドメインを取得していること(本稿で使用するドメインはexample.comとします)
  • IPv4パブリックIP(Elastic IP)がEC2インスタンスに設定されていること
  • EC2インスタンスにDocker、docker-composeがインストールされていること

事前に準備すること

DockerでHTTPS対応のプロキシサーバーを構築するにあたり、事前に以下の設定をしておく必要があります。

  • EC2のインバウンドルールで443ポートを開放する
  • DNSのAレコードを設定する
  • プロキシ用のネットワークを構築する

EC2のインバウンドルールで443ポートを開放する

インバウンドルールを以下のように設定し、443ポートを外部へ公開します。

タイプ プロトコル ポート範囲 ソース
HTTPS TCP 443 0.0.0.0/0
HTTPS TCP 443 ::/0

DNSのAレコードを設定する

DNSの設定方法は利用しているドメイン取得サービスによって異なります。例えばバリュードメインの場合、DNSの設定方法は「DNS情報・URL転送の設定 | VALUE-DOMAIN ユーザーガイド」に記載されています。

DNSのAレコードを以下のように設定します。xx.xx.xx.xxにはEC2インスタンスに割り当てられているIPv4パブリックIPを設定します。

a @ xx.xx.xx.xx
a www xx.xx.xx.xx

上記設定は以下を意味します。

  • example.com(サブドメイン無し)をIPアドレスxx.xx.xx.xxにポイントする
  • www.example.com をIPアドレスxx.xx.xx.xxにポイントする

プロキシ用のネットワークを構築する

プロキシサーバーとWebサーバー間のネットワークは外部との通信を行う必要がありません。そこで
プロキシサーバーとWebサーバー間の 内部ネットワーク を構築するため、EC2のインスタンスにログインし、以下のコマンドを入力します。

$ docker network create --internal sample_proxy_nw

上記コマンドは以下を意味します。

  • --internal: ネットワーク外との通信が行えないネットワークを作成します。
  • sample_proxy_nw: 任意のネットワーク名です。

以下のコマンドを入力し、ネットワークの設定情報がコンソールに出力されていることを確認しましょう。

$ docker network inspect sample_proxy_nw

Dockerコンテナの定義ファイルを作成する

事前準備が完了したら、Dockerコンテナの定義ファイルを作成しましょう。本稿におけるディレクトリ構成は以下のとおりです。

/path/to/dir/

.
├── docker-compose.yml // プロキシサーバーとWebサーバーのコンテナを定義するファイル
└── proxy
    ├── default.conf // プロキシサーバー上にあるNginxのデフォルト定義ファイル
    ├── Dockerfile // プロキシサーバーのイメージを構築するためのファイル
    └── entrypoint.sh // プロキシサーバーにSSL証明書を取得するためのファイル

以下では、各ファイルの内容を解説します。

./docker-compose.yml

docker-compose.ymlでは、以下のコンテナを定義しています。

  • proxy: プロキシサーバー(Nginxベース)
  • web1: Webサーバー(httpdベース)
  • web2: Webサーバー(httpdベース)
version: '3'
services:
  proxy:
    build: ./proxy
    tty: true
    image: sample_proxy
    container_name: sample_proxy
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    ports:
      - "443:443"
    volumes:
      - '/srv/letsencrypt:/etc/letsencrypt'
    networks:
      - default
      - sample_proxy_nw
    depends_on:
      - "web1"
      - "web2"
    command: ["wait-for-it.sh", "sample_web1:80", "--", "./wait-for-it.sh", "sample_web2:80"]
  web1:
    image: httpd
    container_name: sample_web1
    tty: true
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    networks:
      - sample_proxy_nw
  web2:
    image: httpd
    container_name: sample_web2
    tty: true
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    networks:
      - sample_proxy_nw
networks:
  proxy_nw:
    external: true

上記コマンドは以下を意味します。

  • サービスproxyports: 外部からのHTTPSアクセスとproxyサーバーの内部ポートを疎通させるため、443:443を定義します。
  • サービスproxyvolumes: /srv/letsencrypt:/etc/letsencryptを定義します。/etc/letsencryptLet’s Encryptで取得した証明書が生成されるディレクトリ です。
  • networks: 上述の説明で生成したsample_proxy_nwを各サービス(proxy, web1, web2)に定義します。
  • depends_on: コンテナの起動順序を制御するオプションです。 Nginxのproxy_passに設定されているWebサーバーが起動していない状態でプロキシサーバーが起動した場合にエラーとなる ため、web1, web2を設定します。

./proxy/default.conf

./proxy/default.confはNginxのデフォルト定義ファイル(/etc/nginx/conf.d/default.conf)を書き換えるためのファイルです。

server{

    server_name example.com www.example.com;

    proxy_redirect off;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    location / {
        proxy_pass    http://sample_web1/;
    }

    location /example/ {
        proxy_pass    http://sample_web2/;
    }

}

上記設定は以下を意味します。

  • server_name: ユーザーから要求されるHTTPリクエストのヘッダに含まれるHostフィールドとserver_nameが一致した場合、該当するサーバ設定を採用します。Nginxではキャッチオールサーバーとして_を定義することもできますが、 certbot-autoがサーバー情報を正しく取得することができない ため、上記のようにドメイン名を入力します。
  • location: ルートディレクトリ(example.com/)とサブディレクトリ(example.com/example/)にアクセスした際の振り分け先URIを設定します。proxy_passには、http://[コンテナ名]/を設定します。コンテナ名はdocker-compose.ymlのcontainer_nameで設定した名前となります。
    また、http://sample_web1/のように 末尾に/を入れる ことに注意しましょう。例えばlocation /example/において、プロキシパスの末尾に/が含まれていない(http://sample_web2)場合、振り分け先は http://sample_web2/example/となってしまいます。

./proxy/Dockerfile

FROM nginx
COPY default.conf /etc/nginx/conf.d/default.conf
RUN apt-get update && apt-get install -y \
        wget && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*
ADD https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh /usr/local/bin/wait-for-it.sh
RUN chmod +x /usr/local/bin/wait-for-it.sh
ADD https://dl.eff.org/certbot-auto /usr/local/bin/certbot-auto
RUN chmod a+x /usr/local/bin/certbot-auto
RUN certbot-auto --os-packages-only -n
COPY ./entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]

上記設定は以下を意味します。

  • ADD https://dl.eff.org/certbot-auto /usr/local/bin/certbot-auto: Let’s Encryptが発行するSSL/TLSサーバ証明書を自動で取得・更新するツール「 certbot-auto 」をダウンロードします。

./proxy/entrypoint.sh

#!/bin/bash
certbot-auto --nginx -d example.com -d www.example.com -m your-account@gmail.com --agree-tos -n
certbot-auto renew
/bin/bash

上記設定は以下を意味します。

  • --nginx: プロキシサーバーにNginxを使用する場合のオプションです。default.confの設定を自動的に書き換えます。(2017年12月現在、アルファ版のプラグイン)
  • -d example.com -d www.example.com: SSL/TLSサーバ証明書の取得を申請するドメイン名を指定します。
  • -m your-account@gmail.com: アカウントの登録や回復などに使用する電子メールアドレスを指定します。
  • --agree-tos: Let’s Encryptの利用規約に同意します。
  • -n: インタラクティブ設定をオフにします。
  • ./certbot-auto renew: 3ヶ月で失効する SSL/TLSサーバ証明書を自動で更新します。

以下のコマンドを入力してentrypoint.shに 実行権限を付与する ことを忘れないようにしましょう。

$ chmod +x entrypoint.sh

Dockerコンテナを起動する

それでは以下のコマンドを入力してDockerコンテナを起動しましょう。

docker-compose up -d

しばらく時間をおいてから、以下のコマンドを入力します。

docker-compose logs

以下のように出力されていれば成功です。

-------------------------------------------------------------------------------
Congratulations! You have successfully enabled https://example,com and
https://www.example.com

You should test your configuration at:
https://www.ssllabs.com/ssltest/analyze.html?d=example.com
https://www.ssllabs.com/ssltest/analyze.html?d=www.example.com
-------------------------------------------------------------------------------

HTTPSでアクセスする

ブラウザを起動し、実際に以下のURLにアクセスしてみましょう。

Chromeブラウザの場合はデベロッパーツール > Security > View certificateからSSL/TLSサーバ証明書を確認することができます。

「発行元: Let’s Encrypt Authority X3」となっているはずです。

続きを読む