Amazon TranscribeをPython SDKで使ってみた

AWSの音声をテキストに変換(文字起こし)してくれるサービス Amazon Transcribeを使ってみたので備忘録に残しておく。TranscribeはS3にある音声ファイルを指定すると文字起こししてくれて、文字起こしの結果をS3にアップロードしてくれる。

処理の流れは以下のようにした。

  • STEP1: ローカルにある音声ファイルをS3にアップロード
  • STEP2: Transcribeに文字起こし処理を依頼
  • STEP3: 定期的に処理結果を確認
  • STEP4: 処理が完了したら処理結果のURLを取得
  • STEP5: 処理結果をS3からダウンロードし結果を表示 (Pyhon SDK)

基本的には前回のAzure Speech Servicesと同じ流れ。AWSはPython SDKが充実していてS3、Transcribeの処理どちらもSDKで実装できたのが良かった。

準備

S3バケットを作成

TranscribeではS3にアップロードされている音声ファイルを利用するためにS3バケットを作成する。まずはS3のページにアクセスし、右上の「バケットを作成」をクリック。

バケット名を入力する。今回は「speech.tetsis.com」にした。

そのまま「バケットを作成」をクリック。

バケット「speech.tetsis.com」が作成された。

IAMユーザーを作成

Python SDKでAWSのリソースを操作するためのIAMユーザーを作成する。まずはIAMユーザーページにアクセスし、「ユーザーを追加」をクリック。

ユーザー名は「speech-recognition」にした。「プログラムによるアクセス」をチェックし、「次のステップ:アクセス権限」をクリック。

「AmazonS3FullAccess」と「AmazonTranscribeFullAccess」をチェックして、「次のステップ:タグ」をクリック。

あとはそのまま進める。IAMユーザーが作成できたら「アクセスキーID」と「シークレットアクセスキー」が表示されるのでメモしておく。

awsコマンドをインストール

公式ページを参考にawsコマンドをインストールする。詳細な方法は割愛する。

awsコマンドで認証情報を設定

awsコマンドがインストールできたら認証情報を設定する。以下のコマンドを実行し、先ほどメモしておいた「アクセスキーID」と「シークレットアクセスキー」を入力する。

aws configure

Pythonコーディング

今回は以下の環境でPythonを実行する。

– OS: Windows 10
– Python 3.8.1
– pip 20.0.2

最初にPython SDKであるboto3をインストールしておく。

pip install boto3

今回は一つのファイル(aws_speech.py)で実装していく。汎用性の高い関数として実装した「関数コード」と、「main関数コード」の2パートに分けて紹介する。

まずはファイルの先頭にライブラリをインポートするimport文を記述する。

# -*- coding: utf-8 -*-
import os
import uuid
import argparse
import logging
import re
import time
import json

import boto3
from botocore.exceptions import ClientError

STEP1: ローカルにある音声ファイルをS3にアップロード

関数コード

この関数は公式ドキュメントを参考にした。

def upload_file(bucket_name, local_path, local_file_name):
    root_ext_pair = os.path.splitext(local_file_name)
    object_name = root_ext_pair[0] + str(uuid.uuid4()) + root_ext_pair[1] # S3バケット内で唯一のオブジェクト名になるようにランダム文字列(UUID)を挿入しておく
    upload_file_path = os.path.join(local_path, local_file_name)

    # Upload the file
    s3_client = boto3.client('s3')
    try:
        response = s3_client.upload_file(upload_file_path, bucket_name, object_name)
    except ClientError as e:
        print(e)
        return None
    
    print("\nUploading " + local_file_name +" to S3 as object:\n\t" + object_name)
    return object_name

main関数コード

S3バケット名は環境変数で指定するようにした。音声ファイルと言語はプログラム実行時に引数で指定するようにした。

if __name__ == "__main__":
    bucket_name = os.getenv("S3_BUCKET_NAME")

    parser = argparse.ArgumentParser()
    parser.add_argument("-f", "--file", type=argparse.FileType("r", encoding="UTF-8"), required=True)
    parser.add_argument("-l", "--locale", help="e.g. \"en-US\" or \"ja-JP\"", required=True)
    args = parser.parse_args()
    file_name = args.file.name
    locale = args.locale

    # STEP1: ローカルにある音声ファイルをS3にアップロード
    base_dir_pair = os.path.split(file_name)
    local_path = base_dir_pair[0]
    local_file_name = base_dir_pair[1]
    object_name = upload_file(bucket_name, local_path, local_file_name)

実行

実行前に環境変数にS3バケット名を設定する。テキスト変換させる音声は攻殻機動隊SAC26話の名セリフを使ってみる。Netflixで視聴できる。

> $env:S3_BUCKET_NAME = "speech.tetsis.com"
> python .\aws_speech.py -f .\草薙名セリフ.mp3 -l ja-JP

Uploading 草薙名セリフ.mp3 to S3 as object:
        草薙名セリフf6a85e99-8f22-4b95-82a0-77cfd3be80db.mp3

音声ファイルをS3にアップロードできた。実際にAWSコンソール画面を見ると音声ファイルがアップロードされているのが確認できる。

STEP2: Transcribeに文字起こし処理を依頼

関数コード

Transcribeで文字起こしを開始するためにはジョブ名を設定する必要がある。ジョブ名には文字数と種類の制約があるので、それに合わせるためS3にアップロードしたオブジェクト名を変換している。

また、OutputBucketNameには文字起こし結果を格納するS3バケットを指定できる。今回はSTEP1でアップロードしたバケットと同じバケットを使うこととにする。この関数は公式ドキュメントが参考になった。

def start_transcription_job(bucket_name, object_name, language_code):
    job_name = re.sub(r'[^a-zA-Z0-9._-]', '', object_name)[:199] # Transcribeのフォーマット制約に合わせる(最大200文字。利用可能文字は英大文字小文字、数字、ピリオド、アンダーバー、ハイフン)
    media_file_url = 's3://' + bucket_name + '/' + object_name

    client = boto3.client('transcribe')
    response = client.start_transcription_job(
        TranscriptionJobName=job_name,
        LanguageCode=language_code,
        Media={
        'MediaFileUri': media_file_url
        },
        OutputBucketName=bucket_name
    )

    print("\nTranscription start")
    return job_name

main関数コード

if __name__ == "__main__":
    # STEP1のコード(ここでは省略)

    # STEP2: Transcribeに文字起こし処理を依頼
    job_name = start_transcription_job(bucket_name, object_name, locale)

実行

> python .\aws_speech.py -f .\草薙名セリフ.mp3 -l ja-JP

Uploading 草薙名セリフ.mp3 to S3 as object:
        草薙名セリフ04bd9138-da2f-400c-9ea6-7650d524e34e.mp3

Transcription start

これで文字起こし処理を開始するところまでできた。AWSのコンソール画面からTranscribeページを見ると、処理が登録されていることが確認できる。Webブラウザから簡単に処理状態や文字起こし結果を確認できるのがAWSのいいところだと思う。

STEP3: 定期的に処理結果を確認

関数コード

get_transcription_jobのレスポンスのjsonフォーマットは公式ドキュメントを参考にした。

def get_transcription_status(job_name):
    client = boto3.client("transcribe")
    response = client.get_transcription_job(
        TranscriptionJobName=job_name
    )
    status = response["TranscriptionJob"]["TranscriptionJobStatus"]

    print("Transcription status:\t" + status)
    return status

main関数コード

if __name__ == "__main__":
    # STEP1のコード(ここでは省略)
    # STEP2のコード(ここでは省略)

    # STEP3: 定期的に処理結果を確認
    status = ""
    while status not in ["COMPLETED", "FAILED"]: # 処理が完了したら状態が"COMPLETED"、失敗したら"FAILED"になるからそれまでループ
        status = get_transcription_status(job_name) # 処理状態を取得
        time.sleep(3)

実行

> python .\aws_speech.py -f .\草薙名セリフ.mp3 -l ja-JP

Uploading 草薙名セリフ.mp3 to S3 as object:
        草薙名セリフ6d53dece-ea7c-4966-923a-e45687da42b0.mp3

Transcription start
Transcription status:   IN_PROGRESS
Transcription status:   IN_PROGRESS
Transcription status:   IN_PROGRESS
Transcription status:   IN_PROGRESS
Transcription status:   IN_PROGRESS
Transcription status:   IN_PROGRESS
Transcription status:   IN_PROGRESS
Transcription status:   IN_PROGRESS
Transcription status:   IN_PROGRESS
Transcription status:   IN_PROGRESS
Transcription status:   COMPLETED

処理が完了するのを待つところまで実行できた。

STEP4: 処理が完了したら処理結果のURLを取得

関数コード

処理が完了すると、文字起こし結果がS3にアップロードされる。get_transcription_job関数では文字起こし結果を格納しているS3オブジェクトのURLを返してくれる。

def get_transcription_file_url(job_name):
    client = boto3.client("transcribe")
    response = client.get_transcription_job(
        TranscriptionJobName=job_name
    )
    file_url = response["TranscriptionJob"]["Transcript"]["TranscriptFileUri"]

    print("\nFile URL:\n\t" + file_url)
    return file_url

main関数コード

if __name__ == "__main__":
    # STEP1のコード(ここでは省略)
    # STEP2のコード(ここでは省略)
    # STEP3のコード(ここでは省略)

    # STEP4: 処理が完了したら処理結果のURLを取得
    file_url = get_transcription_file_url(job_name)

実行

> python .\aws_speech.py -f .\草薙名セリフ.mp3 -l ja-JP

Uploading 草薙名セリフ.mp3 to S3 as object:
        草薙名セリフd3496aa1-11f8-4f90-aeb8-e4f705467a0d.mp3

Transcription start
Transcription status:   IN_PROGRESS
Transcription status:   IN_PROGRESS
...(中略)...
Transcription status:   IN_PROGRESS
Transcription status:   IN_PROGRESS
Transcription status:   COMPLETED

File URL:
        https://s3.ap-northeast-1.amazonaws.com/speech.tetsis.com/d3496aa1-11f8-4f90-aeb8-e4f705467a0d.mp3.json

最後に表示されているURLが文字起こし結果が格納されているS3オブジェクト。

STEP5: 処理結果をS3からダウンロードし結果を表示

関数コード

download_file関数は公式ドキュメントを参考にした。ダウンロードしたjsonファイルのデータフォーマットも公式ドキュメントを参考にした。

def download_file(bucket_name, object_name):
    s3 = boto3.client('s3')
    s3.download_file(bucket_name, object_name, object_name)
    print("\nDownloading S3 object:\n\t" + object_name)

def get_transcript_from_file(file_name):
    with open(file_name, "r", encoding="utf-8") as f:
        df = json.load(f)

    transcript = ""
    for result in df["results"]["transcripts"]:
            transcript += result["transcript"] + "\n"
    
    print("\nTranscription result:\n" + transcript)
    return transcript

main関数コード

if __name__ == "__main__":
    # STEP1のコード(ここでは省略)
    # STEP2のコード(ここでは省略)
    # STEP3のコード(ここでは省略)
    # STEP4のコード(ここでは省略)

    # STEP5: 処理結果をS3からダウンロードし結果を表示 (Pyhon SDK)
    result_object_name = file_url[file_url.find(bucket_name) + len(bucket_name) + 1:]
    download_file(bucket_name, result_object_name) # ここでダウンロード
    get_transcript_from_file(result_object_name)

実行

> python .\aws_speech.py -f .\草薙名セリフ.mp3 -l ja-JP

Uploading 草薙名セリフ.mp3 to S3 as object:
        草薙名セリフ6aa4385a-f760-4c76-9b03-2cf015593a59.mp3

Transcription start
Transcription status:   IN_PROGRESS
Transcription status:   IN_PROGRESS
...(中略)...
Transcription status:   IN_PROGRESS
Transcription status:   IN_PROGRESS
Transcription status:   COMPLETED

File URL:
        https://s3.ap-northeast-1.amazonaws.com/speech.tetsis.com/6aa4385a-f760-4c76-9b03-2cf015593a59.mp3.json  

Downloading S3 object:
        6aa4385a-f760-4c76-9b03-2cf015593a59.mp3.json

Transcription result:
だ けど 私 は 情報 の 並列 化 の 果て に 後 を 取り戻す ため の 一つ の 可能 性 を 見つけ た わ ちなみに その 答え は 好奇 心 多分 ね

どのセリフを文字起こししたのか、まあ分かる、よね。単語ごと(?)にスペース区切りされているから少し見づらいけどほとんど正確に文字に変換してくれてる(「後」は本当は「個」。セリフの重要な部分なだけに残念)。「ちなみに その 答え は」は別の人のセリフなんだけどそれでもちゃんと認識しててすごい。

全コード

最後に今回のコードの全体を載せておく。

# -*- coding: utf-8 -*-
import os
import uuid
import argparse
import logging
import re
import time
import json

import boto3
from botocore.exceptions import ClientError

def upload_file(bucket_name, local_path, local_file_name):
    root_ext_pair = os.path.splitext(local_file_name)
    object_name = root_ext_pair[0] + str(uuid.uuid4()) + root_ext_pair[1] # S3バケット内で唯一のオブジェクト名になるようにランダム文字列(UUID)を挿入しておく
    upload_file_path = os.path.join(local_path, local_file_name)

    # Upload the file
    s3_client = boto3.client('s3')
    try:
        response = s3_client.upload_file(upload_file_path, bucket_name, object_name)
    except ClientError as e:
        print(e)
        return None
    
    print("\nUploading " + local_file_name +" to S3 as object:\n\t" + object_name)
    return object_name

def download_file(bucket_name, object_name):
    s3 = boto3.client('s3')
    s3.download_file(bucket_name, object_name, object_name)
    print("\nDownloading S3 object:\n\t" + object_name)

def start_transcription_job(bucket_name, object_name, language_code):
    job_name = re.sub(r'[^a-zA-Z0-9._-]', '', object_name)[:199] # Transcribeのフォーマット制約に合わせる(最大200文字。利用可能文字は英大文字小文字、数字、ピリオド、アンダーバー、ハイフン)
    media_file_url = 's3://' + bucket_name + '/' + object_name

    client = boto3.client('transcribe')
    response = client.start_transcription_job(
        TranscriptionJobName=job_name,
        LanguageCode=language_code,
        Media={
        'MediaFileUri': media_file_url
        },
        OutputBucketName=bucket_name
    )

    print("\nTranscription start")
    return job_name

def get_transcription_status(job_name):
    client = boto3.client("transcribe")
    response = client.get_transcription_job(
        TranscriptionJobName=job_name
    )
    status = response["TranscriptionJob"]["TranscriptionJobStatus"]

    print("Transcription status:\t" + status)
    return status

def get_transcription_file_url(job_name):
    client = boto3.client("transcribe")
    response = client.get_transcription_job(
        TranscriptionJobName=job_name
    )
    file_url = response["TranscriptionJob"]["Transcript"]["TranscriptFileUri"]

    print("\nFile URL:\n\t" + file_url)
    return file_url

def get_transcript_from_file(file_name):
    with open(file_name, "r", encoding="utf-8") as f:
        df = json.load(f)

    transcript = ""
    for result in df["results"]["transcripts"]:
            transcript += result["transcript"] + "\n"
    
    print("\nTranscription result:\n" + transcript)
    return transcript

if __name__ == "__main__":
    bucket_name = os.getenv("S3_BUCKET_NAME")

    parser = argparse.ArgumentParser()
    parser.add_argument("-f", "--file", type=argparse.FileType("r", encoding="UTF-8"), required=True)
    parser.add_argument("-l", "--locale", help="e.g. \"en-US\" or \"ja-JP\"", required=True)
    args = parser.parse_args()
    file_name = args.file.name
    locale = args.locale

    # STEP1: ローカルにある音声ファイルをS3にアップロード
    base_dir_pair = os.path.split(file_name)
    local_path = base_dir_pair[0]
    local_file_name = base_dir_pair[1]
    object_name = upload_file(bucket_name, local_path, local_file_name)

    # STEP2: Transcribeに文字起こし処理を依頼
    job_name = start_transcription_job(bucket_name, object_name, locale)

    # STEP3: 定期的に処理結果を確認
    status = ""
    while status not in ["COMPLETED", "FAILED"]: # 処理が完了したら状態が"COMPLETED"、失敗したら"FAILED"になるからそれまでループ
        status = get_transcription_status(job_name) # 処理状態を取得
        time.sleep(3)

    # STEP4: 処理が完了したら処理結果のURLを取得
    file_url = get_transcription_file_url(job_name)

    # STEP5: 処理結果をS3からダウンロードし結果を表示 (Pyhon SDK)
    result_object_name = file_url[file_url.find(bucket_name) + len(bucket_name) + 1:]
    download_file(bucket_name, result_object_name) # ここでダウンロード
    get_transcript_from_file(result_object_name)

サブドメイン分割で複数サイトをEKSクラスタに構築してみた(ALB Ingress Controller, ExternalDNS, 独自ドメイン, HTTPS対応)

EKSクラスタ内で動作しているpodを外部公開するためのエンドポイントとしてALBを使う場合の構築手順をまとめておく。
ALB Ingress Controllerを使うことで、ingressをapplyしたときに自動でALBを構築してくれるのですごく便利だった。
また、ingressで任意のドメインを指定するとALBのhost-based routingに反映してくれるので、サブドメインを変えて複数のサイト公開も楽にできた。
加えて、ExternalDNSを使うと自分が管理しているドメインのレコード追加を自動でやってくれる。
SSL証明書をACMに用意しておくと、簡単にHTTPS化できたので、HTTPS対応についても最後にまとめておく。

アーキテクチャ

今回使うアプリケーションはDjangoとその前段にリバースプロキシとしてnginxを配置したシンプルなもの。EKSクラスタ内にはその他にALBにルール反映してくれるALB Inress ControllerとRoute 53にレコード反映してくれるExternalDNSのpodが起動している。SSL証明書発行にはACMを利用。

アーキテクチャ

EKSクラスタ構築

公式サイトの方法を参考にクラスタ構築。
eksctl で構築するのが便利。

eksctl create cluster \
--name test-cluster \
--version 1.14 \
--region ap-northeast-1 \
--nodegroup-name standard-workers \
--node-type t3.medium \
--nodes 2 \
--nodes-min 1 \
--nodes-max 4 \
--managed

(参考)EC2インスタンスのタイプによって付与可能なIPアドレス数が異なるため、podをたくさん起動する場合はworker nodeのインスタンスタイプに注意すること。
IPアドレス数の一覧はこちら

ALB Ingress Controllerのデプロイ

基本は公式サイトの手順通りに実施する。

EKSクラスタのサブネットにタグが付いていることを確認

  • VPC 内のすべてのサブネット
    • kubernetes.io/cluster/<cluster-name>shared
  • VPC のパブリックサブネット
    • kubernetes.io/role/elb1
  • VPC 内のプライベートサブネット
    • kubernetes.io/role/internal-elb1

IAM ポリシーを作成

IAMポリシーファイルをダウンロード

curl -O https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.1.3/docs/examples/iam-policy.json

IAMポリシーを作成

aws iam create-policy \
--policy-name ALBIngressControllerIAMPolicy \
--policy-document file://iam-policy.json

EKSクラスタのワーカーノードの IAM ロール名を取得

kubectl -n kube-system describe configmap aws-auth

出力結果の<アカウントID>と<ロール名>を控えておく。

Name:         aws-auth
Namespace:    kube-system
Labels:       <none>
Annotations:  <none>

Data
====
mapRoles:
----
- groups:
  - system:bootstrappers
  - system:nodes
  rolearn: arn:aws:iam::<アカウントID>:role/<ロール名>
  username: system:node:{{EC2PrivateDNSName}}

mapUsers:
----
[]

Events:  <none>

EKSクラスタのワーカーノードにIAMポリシーをアタッチ

aws iam attach-role-policy \
--policy-arn arn:aws:iam::<アカウントID>:policy/ALBIngressControllerIAMPolicy \
--role-name <ロール名>

ALB Ingress Controller で使用するサービスアカウント、クラスターロール、クラスターロールバインディングを作成

kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.1.3/docs/examples/rbac-role.yaml

ALB Ingress Controller をデプロイ

ここは公式サイトとは違う方法でデプロイする。
公式サイトではmanifestをデプロイした後に環境に合わせて編集しているが、今回は最初にデプロイ用manifestをダウンロードし、ファイルを編集した後にデプロイする。

ALB-Ingress-Controllerのmanifestをダウンロード

curl -O https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.1.3/docs/examples/alb-ingress-controller.yaml

Manifestを編集する。以下のパラメータのコメントアウトを外し、書き換える。

    spec:
      containers:
      - args:
        - --cluster-name=test-cluster
        - --aws-vpc-id=<VPC ID>
        - --aws-region=ap-northeast-1

(参考)VPC IDを確認するには以下のコマンドを実行する。

aws ec2 describe-vpcs

デプロイ

kubectl apply -f alb-ingress-controller.yaml

以下のコマンドを実行してエラーが表示されなければデプロイ成功

kubectl logs -n kube-system -f $(kubectl get po -n kube-system | egrep -o 'alb-ingress-controller[A-Za-z0-9-]+')

ExternalDNSのデプロイ

ALBのエンドポイントを自分が管理しているRoute53にailiasとして反映させてくれる ExternalDNS というOSSがあるので使ってみる。

IAM ポリシーを作成

IAMポリシーファイルを作成。公式サイトのチュートリアルから流用

{
 "Version": "2012-10-17",
 "Statement": [
   {
     "Effect": "Allow",
     "Action": [
       "route53:ChangeResourceRecordSets"
     ],
     "Resource": [
       "arn:aws:route53:::hostedzone/*"
     ]
   },
   {
     "Effect": "Allow",
     "Action": [
       "route53:ListHostedZones",
       "route53:ListResourceRecordSets"
     ],
     "Resource": [
       "*"
     ]
   }
 ]
}

IAMポリシーを作成

aws iam create-policy \
--policy-name ExternalDNSIAMPolicy \
--policy-document file://external-dns-iam-policy.json

EKSクラスタのワーカーノードにIAMポリシーをアタッチ

aws iam attach-role-policy \
--policy-arn arn:aws:iam::<アカウントID>:policy/ExternalDNSIAMPolicy \
--role-name <ロール名>

ExternalDNS をデプロイ

ExternalDNSのmanifestをダウンロード

curl -O https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.1.3/docs/examples/external-dns.yaml

Manifestを編集。example.comには自分で管理しているFQDNに変更すること。ダウンロードしたファイルではpolicyオプションがupsert-onlyになっているが、これはレコード追加だけすることを表す。今回はingressの内容に合わせて適宜レコード削除もしてほしいのでpolicyオプションは使わない。

args:
- --domain-filter=example.com
#- --policy=upsert-only # コメントアウトまたは削除

デプロイ

kubectl apply -f external-dns.yaml

確認

kubectl logs -f $(kubectl get po | egrep -o 'external-dns[A-Za-z0-9-]+')

以下のように表示されればOK

time="2019-12-13T14:48:31Z" level=info msg="Created Kubernetes client https://10.100.0.1:443"

app1のmanifestを作成

ここからようやくアプリケーションをデプロイすることができる。
まずはdeploymentとserviceとingressのmanifestを用意する。

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: app1-deployment
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: app1
    spec:
      containers:
        - image: tetsis/simple-nginx-django-app
          name: app1
          ports:
            - containerPort: 8080
          env:
            - name: APPLICATION_NAME
              value: app1
        - image: tetsis/simple-nginx-django-proxy
          name: app1-proxy
          ports:
            - name: app1-port
              containerPort: 80
apiVersion: v1
kind: Service
metadata:
  name: app1-service
spec:
  ports:
    - port: 80
      targetPort: app1-port
      protocol: TCP
  type: NodePort
  selector:
    app: app1
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
  labels:
    app: ingress
spec:
  rules:
    - host: app1.example.com
      http:
        paths:
          - path: /*
            backend:
              serviceName: app1-service
              servicePort: 80

example.comには自分が所有するドメインを指定する。

app1のデプロイ

kubectl apply -f app1-deployment.yaml 
kubectl apply -f app1-service.yaml 
kubectl apply -f ingress.yaml

ALB Ingress Controllerで作られたALBを確認

kubectl get ingress
NAME      HOSTS              ADDRESS                                                                     PORTS   AGE
ingress   app1.example.com   d7f12bb1-default-ingress-e8c7-1478980459.ap-northeast-1.elb.amazonaws.com   80      38s

Route 53にレコードが作られたことを確認

コンソール画面もしくは以下のコマンドでAliasレコードが作成されていることを確認する。

aws route53 list-resource-record-sets --hosted-zone-id /hostedzone/<ホストゾーンID>

ページにアクセスしてみる

ALBの起動とRoute 53のDNSレコード情報が反映されるまで少し待って、ブラウザで「http://app1.example.com」にアクセスし、以下のように表示されればOK。
(example.comは自分のドメインを指定)

This application is "app1".

サブドメイン分割でapp2を追加

さっきはapp1.example.comというドメインでアプリケーションをデプロイしたが、次はapp2.example.comドメインでアプリケーションを追加デプロする。
deploymentとserviceはapp1と同様にmanifestを作成する。
ingressについては 、app1とapp2で同じALBを使うので先程作成したmanifestに追記する。

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: app2-deployment
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: app2
    spec:
      containers:
        - image: tetsis/simple-nginx-django-app
          name: app2
          ports:
            - containerPort: 8080
          env:
            - name: APPLICATION_NAME
              value: app2
        - image: tetsis/simple-nginx-django-proxy
          name: app2-proxy
          ports:
            - name: app2-port
              containerPort: 80
apiVersion: v1
kind: Service
metadata:
  name: app2-service
spec:
  ports:
    - port: 80
      targetPort: app2-port
      protocol: TCP
  type: NodePort
  selector:
    app: app2
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
  labels:
    app: ingress
spec:
  rules:
    - host: app1.example.com
      http:
        paths:
          - path: /*
            backend:
              serviceName: app1-service
              servicePort: 80
    - host: app2.example.com
      http:
        paths:
          - path: /*
            backend:
              serviceName: app2-service
              servicePort: 80

app2のデプロイ

kubectl apply -f app2-deployment.yaml 
kubectl apply -f app2-service.yaml 
kubectl apply -f ingress.yaml

ページにアクセスしてみる

ブラウザで「http://app2.example.com」にアクセスして以下の文字が表示されればOK。(もちろんドメインは環境に合わせること)

This application is "app2".

HTTPS化

公式ページを参考に、ingress.yamlの annotations 句に一行追加する。
尚、使用するドメインのSSL証明書は事前にACMで発行しておく。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]' # 追加
  labels:
    app: ingress
spec:
  rules:
    - host: app1.example.com
      http:
        paths:
          - path: /*
            backend:
              serviceName: app1-service
              servicePort: 80
    - host: app2.example.com
      http:
        paths:
          - path: /*
            backend:
              serviceName: app2-service
              servicePort: 80

忘れずにapplyする。

kubectl apply -f ingress.yaml

ブラウザで「https://app1.example.com」、 「https://app2.example.com」 にアクセスして、ページが表示されることを確認する。

リソースの削除

EKSクラスタは起動しておくだけでお金がかかるので、検証利用の場合は忘れずにクラスタを削除を実施する。


K8sリソースを削除

kubectl delete -f ingress.yaml
kubectl delete -f app1-service.yaml
kubectl delete -f app2-service.yaml
kubectl delete -f app1-deployment.yaml
kubectl delete -f app2-deployment.yaml

IAMポリシーをデタッチ

aws iam detach-role-policy --policy-arn arn:aws:iam::<アカウントID>:policy/ExternalDNSIAMPolicy --role-name <ロール名>
aws iam detach-role-policy --policy-arn arn:aws:iam::<アカウントID>:policy/ALBIngressControllerIAMPolicy --role-name <ロール名>

IAMポリシーを削除

 aws iam delete-policy --policy-arn arn:aws:iam::<アカウントID>:policy/ExternalDNSIAMPolicy
 aws iam delete-policy --policy-arn arn:aws:iam::<アカウントID>:policy/ALBIngressControllerIAMPolicy

EKSクラスタを削除

eksctl delete cluster --region=ap-northeast-1 --name=test-cluster

Packerで作成したWebサーバAMIをTerraformで構築する

やりたいこと

  1. PackerでAmazon AMIを作成する
    • 下図の左側に示すように、jsonファイルを用意し、Packerを実行してAMIを作成する
    • 今回作成するAMIは、HTTPアクセスすると任意のメッセージを表示するApacheプロセスが自動起動するという簡単なもの
  2. TerraformでEC2インスタンスを起動する
    • 下図の右側に示すように、Packerで作成したAMIを使ってEC2インスタンスを構築するtfファイルを作り、Terraformを実行する

検証環境

  • Packer: v1.3.4
  • Terraform: v0.11.11
  • AWSリージョン: ap-northeast-1(東京)

前準備:IAMでAPI実行用のグループとユーザを作成

まずはIAMグループページでグループを作成。グループ名は任意でOK

今回の検証ではEC2のみの利用であるため、必要なポリシーは「AmazonEC2FullAccess」のみ

次にIAMユーザページでユーザを作成。ユーザ名は任意でOK。アクセスの種類は「プログラムによるアクセス」を選択

先程作成したグループをこのユーザに適用する

タグはなんでもよいが「Name」タグを作っておくのが一般的。

最後に「アクセスキー ID」と「シークレットアクセスキー」が表示される。この二つのキーを忘れずに控えておくこと

PackerでAMI作成

jsonファイル作成

以下のjsonファイルを作成する

今回の例ではファイル名は「simple-http-ami.json」としている

後半の”provisioners”句で、Apacheをインストールして、ドキュメントルートに任意のメッセージが書かれたテキストファイルを配置して、自動起動を有効化している

{
    "variables": {
        "aws_access_key": "{{env `AWS_ACCESS_KEY_ID`}}",
        "aws_secret_key": "{{env `AWS_SECRET_ACCESS_KEY`}}",
        "region":         "ap-northeast-1"
    },
    "builders": [
        {
            "access_key": "{{user `aws_access_key`}}",
            "ami_name": "packer-linux-aws-demo-{{timestamp}}",
            "instance_type": "t2.micro",
            "region": "{{user `region`}}",
            "secret_key": "{{user `aws_secret_key`}}",
            "source_ami_filter": {
              "filters": {
                  "name": "amzn2-ami-hvm-*-x86_64-gp2"
              },
              "owners": ["137112412989"],
              "most_recent": true
            },
            "ssh_username": "ec2-user",
            "type": "amazon-ebs"
        }
    ],
    "provisioners": [{
        "type": "shell",
        "inline": [
            "sleep 30",
            "sudo yum -y install httpd",
            "sudo bash -c \"echo 'This is a simple web page.<br/>The AMI is created by Packer and this server is built by Terraform.' >> /var/www/html/index.html\"",
            "sudo systemctl enable httpd"
        ]
    }]
}

jsonファイルの構文チェック

packer validate コマンドを使って構文チェックする

「Template validated successfully.」と表示されればOK

$ packer validate simple-http-ami.json
Template validated successfully.

AMI作成

まずはアクセスキー等を環境変数に設定する

$ export AWS_ACCESS_KEY_ID=[アクセスキーID]
$ export AWS_SECRET_ACCESS_KEY=[シークレットアクセスキー]

packer build コマンドでAMIを作成する

$ packer build simple-http-ami.json
(中略)
==> Builds finished. The artifacts of successful builds are:
--> amazon-ebs: AMIs were created:
ap-northeast-1: ami-0b8314be1d52e879e

成功すれば、最後の行にがAMI IDが表示される

TerraformでEC2インスタンス構築

tfファイル作成

「simple-http-instance.tf」ファイルを作成し、以下の内容を記述する

(この記事ではEC2インスタンスのみ構築する方法を記載している。一応これだけでもインスタンスは作成される。しかし本来はVPCやサブネット、セキュリティグループを作成する必要がある。VPC等を含めたterraformのコードについてはこちらを参考にしてほしい)

variable "access_key" {}
variable "secret_key" {}
variable "ami" {}
 
provider "aws" {
    access_key = "${var.access_key}"
    secret_key = "${var.secret_key}"
    region = "ap-northeast-1"
}
 
resource "aws_instance" "app_main" {
    ami           = "${var.ami}"
    associate_public_ip_address = "true"
    instance_type = "t2.micro"
 
    tags = {
        Name = "simple http instance"
    }
}
 
output "public_ip" {
  value = "${aws_instance.app_main.public_ip}"
}

Terraform実行確認

まずは環境変数を設定する

$ export TF_VAR_access_key=[アクセスキーID]
$ export TF_VAR_secret_key=[シークレットアクセスキー]
$ export TF_VAR_ami=ami-0b8314be1d52e879e (先程Packerで作成したAMI)

terraform plan コマンドで実行内容を確認する

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
(中略)
Plan: 1 to add, 0 to change, 0 to destroy.

Terraform実行

teffaform apply コマンドでEC2インスタンスを構築する

$ terraform apply
(中略)
Plan: 1 to add, 0 to change, 0 to destroy.
 
Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.
 
  Enter a value: yes
(中略)
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
 
Outputs:
 
public_ip = x.x.x.x

起動したEC2インスタンスはデフォルトのセキュリティグループが適用されている。デフォルトのセキュリティグループにて、検証環境から80番ポートのアクセスを許可し、ブラウザでアクセスすると以下の文言が記載されたWebページが表示される。

This is a simple web page.
The AMI is created by Packer and this server is built by Terraform.