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

Amplify ConsoleのカスタムドメインをValue-DomainからRoute53へ移管する

前回「AWSにフルサーバーレスなWebサイト運用環境を作る(Gatsby + Amplify + CodeCommit)」で、Amplify Consoleを使ってフルサーバーレスで爆速Webサイトを作りました。


その時はAmplifyのカスタムドメインの管理を外部レジストラ(実はValue-Domain)にCNAME設定したのですが、今回はそのAmplifyで利用中のドメイン管理をValue-DomainからAWS Route53へ移管します。


といってもほとんど公式ガイドの手順の通りなのですが、多少Amplify特有の部分もあるので記載します。1〜2週間かかる可能性があると公式には記載がありましたが、やってみると半日程度でスムーズに移管できました。


Value-Domain側の移管手順を確認する

まずはValue-Domainのドメイン移管手順をざっと確認します。

参考 https://www.value-domain.com/userguide/manual/transferother/


Route53へホストゾーンを設定する

ここからはAWS のRoute53に移管方法が書いてあるのでそれを一読してから進めます。

参考サイト : Route 53 を使用中のドメインの DNS サービスにする


現在の DNS サービスプロバイダから現在の DNS 設定を取得する (オプション、ただし推奨)

念のため(ミスった時用)に、Value-Domain上のドメイン設定をメモしておきます。以下は例です。

cname (対象ドメイン名). master.(サブドメイン).amplifyapp.com.
cname www.(対象ドメイン名). master.(サブドメイン).amplifyapp.com.
cname stg.(対象ドメイン名). stg.(サブドメイン).amplifyapp.com.
cname _(検証文字列).(対象ドメイン名). _(acm検証用サブドメイン).acm-validations.aws.

Route53にホストゾーンを作成する

AWS 管理コンソールへサインインしてRoute53を開き、ホストゾーンを作成します。

domain name: (対象ドメイン名)
option: (設定不要)
type: public host zone

レコードを作成する

Amplifyのカスタムドメイン用レコードの作成はAmplify Consoleで行います。Route53からではAmplify Console用のAlias Aレコード設定が出来ないので注意が必要です。しかもすでに外部レジストラ用にAmplify にカスタムドメイン設定が済んでいると、それをRoute53へ反映させる方法がわからずちょっと悩みました。


正解は、「ステップ2でホストゾーンを作った後、Amplify Consoleでドメイン設定を更新する(例:いったん無効化→更新→再度有効化→更新)と、Route53のホストゾーンにレコードが追加されました。

Amplifyコンソール > アプリ名 > アプリの設定 > ドメイン管理

サブドメインの管理を選び、(対象ドメイン名)をいったんdisableしてからenableする。


Amplify のDNS設定確認

実際にRoute53のホストゾーンにAmplifyのレコードが登録されたことを確認します。

レコード名
タイプ
ルーティングポリシー
差別化要因
値/トラフィックのルーティング先

(対象ドメイン名) A   シンプル    -   
(サブドメイン).cloudfront.net.

(対象ドメイン名) NS  シンプル    -   
ns-NNN.awsdns-00.net.
ns-NNN.awsdns-13.com.
ns-NNN.awsdns-39.co.uk.
ns-NNN.awsdns-31.org.

(対象ドメイン名) SOA シンプル    -   
ns-NNN.awsdns-00.net. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400

stg.(対象ドメイン名) CNAME   シンプル    -   
(サブドメイン).cloudfront.net

www.(対象ドメイン名) CNAME   シンプル    -   
(サブドメイン).cloudfront.net

NameServerをValue-DomainからRoute53へ切り替える

移管前にNameServerをRoute53に切り替えます。


TL の設定を下げる

Value Domain側のDNSレコード編集でTTLを十分短く設定します。(今回はすでに300secに設定してあるので、変更しませんでした)


古い TTL の有効期限切れを待つ

今回はすでに300secに設定してあるので、待ちませんでしたが、TTLを長く設定していた場合、時間がかかります。


現在の DNS サービスプロバイダで NS レコードを更新して Route 53 ネームサーバーを使用する

Route53のHosted ZoneでNSレコードの値を確認する。以下は例。

ns-nnn.awsdns-00.net.
ns-nnn.awsdns-13.com.
ns-nnn.awsdns-39.co.uk.
ns-nnn.awsdns-31.org.

Value-domainのネームサーバー設定をRoute53のNSレコードの値に変更します。

Value-domain > コントロールパネル >(ドメイン名) > DNS > ネームサーバーの設定

ネームサーバーの値をRoute53のものに変更して更新します。


ドメインのトラフィックの監視

変更後、ちゃんとドメインへアクセス出来ていることを確認する。


NS レコードの TTL を高い値に戻す

必要に応じて、NSレコードのTTL設定を元に戻します。


ドメインをAmazon Route53へ移管する

ここからが本当の移管手続きです。移管にはValue-Domainが発行する認証鍵が必要です。そのためにValue-Domainのwhois代理公開機能を無効化する必要があります


Value-domain のwhois の代理公開を無効にします。

whois で管理者情報からプライバシーを漏洩させないためにドメインのwhois情報をValue-Domainに代理情報で公開してもらっている場合があります。その場合、認証キーを取得するために無効化が必要です。

Value-domain > コントロールパネル >(ドメイン名) > ドメイン >ドメインの設定・操作>ドメインの登録情報変更[eNom]

「 名義(WHOIS)の代理公開」を無効化します。


Valuedomain の認証鍵をメモする

参考: http://blog.10rane.com/2014/09/16/transferred-a-domain-to-route53/

Value-domain > コントロールパネル >(ドメイン名) > ドメイン >ドメインの設定・操作>ドメインの登録情報変更[eNom]

画面の下にある認証鍵情報をメモします。

認証鍵(Authorization Info)   (認証用文字列)

Amazon Route53 で「ドメインの移管」を実施する

Amazon Route53の「ドメイン」画面で、「ドメインの移管」を選択します。


移管したいドメインを検索し、カートにいれます。
「認証コードとネームサーバー」に先ほど取得した認証鍵を入力します。
「ネームサーバーのオプション」は「ドメインと同じ名前のRoute53ホストゾーンからネームサーバーをインポートする」を選びます。
「ホストゾーン」へ予め設定した(対象ドメイン名)のホストゾーンを選択します。
「1. ドメインのお問い合わせ詳細」に管理者情報を再度入力します。このタイミングでAmazonによる代理情報を設定することができます。


登録後、AWSから管理者メールアドレスの確認メールが届くのでリンクをクリックして確認します。

さらにしばらくすると移行元のValue-Domainから移行確認メールが届くのでメール内の認証ページへアクセスして承認します。今回は2ー3時間後に届きました。


その後、Route53で「ドメイン」を確認すると移管が終了した状態になっていました。

whois (移管したドメイン名)を確認し、amazonの情報になったことを確認して完了です。

間に会社間の手続きが入るのでもっと時間がかかるかと思っていましたが、思った以上にスムーズ・短時間で移管できました。

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

CloudFormationで作成するEC2が指定した起動テンプレートの設定で上書きされない

事象

CloudFormation(以下CFn)を用いて、起動テンプレートからEC2 インスタンス(以下EC2)を作成する際に、起動テンプレートの設定で上書きされませんでした。
なお、CFnを用いず、マネジメントコンソール(以下マネコン)にてテンプレートからEC2を起動する場合は、問題無く設定内容が反映されます。

以下画像のような設定で起動テンプレートを作成します。
※起動テンプレート自体はCFnで作成してもマネコンから作成しても変わりません。
2020-08-30_18h05_40.png

CFnでこの起動テンプレートを指定して作成したEC2の設定は以下画像のようになります。
実際にできたEC2と起動テンプレートで設定内容が異なっています。

使用したCFnテンプレート
起動テンプレートとEC2を同時に作成する場合
AWSTemplateFormatVersion: "2010-09-09"
Description: "Template Settings Overwrite Test"
Parameters:
  ImageId:
    Default: "ami-0cc75a8978fbbc969"
    MinLength: 1
    Type: "String"
  KeyPairName:
    MinLength: 1
    Type: "AWS::EC2::KeyPair::KeyName"
  SubnetIdForEth0:
    MinLength: 1
    Type: "AWS::EC2::Subnet::Id"
  SecurityGroupIdsForEth0:
    MinLength: 1
    Type: "List<AWS::EC2::SecurityGroup::Id>"
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      -
        Label:
          default: "Basic Configuration"
        Parameters:
          - "InstanceType"
          - "ImageId"
          - "KeyPairName"
      -
        Label:
          default: "NetworkInterface Configuration For Eth0"
        Parameters:
          - "SubnetIdForEth0"
          - "SecurityGroupIdsForEth0"
Resources:
  EC2LaunchTemplate:
    Type: "AWS::EC2::LaunchTemplate"
    DeletionPolicy: "Delete"
    Properties:
      LaunchTemplateName: "test_EC2LaunchTemplate"
      LaunchTemplateData:
        BlockDeviceMappings:
          - DeviceName: "/dev/xvda"
            Ebs:
              DeleteOnTermination: true
              Encrypted: false
              VolumeSize: 8
              VolumeType: "gp2"
        CreditSpecification:
          CpuCredits: "unlimited"
        DisableApiTermination: true
        EbsOptimized: true
        ImageId:
          Ref: "ImageId"
        InstanceInitiatedShutdownBehavior: "terminate"
        InstanceType: "t3.nano"
        KeyName:
          Ref: "KeyPairName"
        Monitoring:
          Enabled: true
        NetworkInterfaces:
          - AssociatePublicIpAddress: true
            DeleteOnTermination: true 
            DeviceIndex: 0
            Groups:
              Ref: "SecurityGroupIdsForEth0"
            SubnetId:
              Ref: "SubnetIdForEth0"
        TagSpecifications:
          - ResourceType: "instance"
            Tags:
              - Key: "Name"
                Value: "test"
          - ResourceType: "volume"
            Tags:
              - Key: "Name"
                Value: "test-root"
  EC2Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      LaunchTemplate:
        LaunchTemplateId: !Ref 'EC2LaunchTemplate'
        Version: !GetAtt 'EC2LaunchTemplate.LatestVersionNumber'
Outputs:
  EC2LaunchTemplate:
    Value: !Ref 'EC2LaunchTemplate'
    Export:
      Name: !Sub '${AWS::StackName}-LaunchTemplateID'
上のCFnテンプレートで作成した起動テンプレートを別スタックで指定してEC2を作成する場合
AWSTemplateFormatVersion: "2010-09-09"
Description: "Overwriting test of template settings to be imported"
Parameters:
  EC2LaunchTemplateStackName:
    MinLength: 1
    Type: "String"
Resources:
  EC2Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      LaunchTemplate:
        LaunchTemplateId:
          Fn::ImportValue:
            !Sub '${EC2LaunchTemplateStackName}-LaunchTemplateID'
        Version: "1"
マネコンで作成した起動テンプレートを指定してEC2を作成する場合
AWSTemplateFormatVersion: "2010-09-09"
Description: "Overwriting test of template settings to be imported"
Resources:
  EC2Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      LaunchTemplate:
        LaunchTemplateId: "lt-xxxxxxxxxxxxxxxxx"
        Version: "1"


2020-08-30_18h22_58.png
2020-08-30_18h23_53.png

AWS::EC2::Instance ドキュメントLaunchTemplate項目には以下記載があります。

インスタンスの起動に使用する起動テンプレート。AWS CloudFormation テンプレートで指定したパラメータはすべて、起動テンプレート内の同じパラメータを上書きします。

記載通りならEC2を作成するCFnで起動テンプレートを指定した場合は、設定内容が上書きされる筈なので、完全に謎です。
ただし、全ての項目が上書きされないわけではなく、以下の項目が上書きされないようです。

  • DisableApiTermination(終了保護)
  • EbsOptimized(EBS 最適化インスタンス)※
  • InstanceInitiatedShutdownBehavior(シャットダウン動作)
  • Monitoring(CloudWatch モニタリングの詳細)

※EBS 最適化インスタンス項目については、デフォルトでEBS最適化がサポートされるインスタンスタイプの場合、false指定であってもEBS最適化が適用されます。false にすると マネコン上の表示やawscliで参照した結果は false になるもののEBS最適化は適用状態となります。

結論

AWSサポートに問い合わせたところ、不具合との事でした。

ご連絡いただきました事象が再現することを確認いたしました。
CloudFormation によるパラメータの暗黙的な上書きが発生していることが分かりました。
なお、この動作につきましてはドキュメント化されていない動作となり、将来的に変更される可能性も考えられます。
AWSサポート回答より※2020/08/27時点

回避策

現状では、EC2を作成するCFnテンプレートのAWS::EC2::Instanceリソース側で、明示的にプロパティを指定すれば起動テンプレートを指定していても任意の設定が可能です。

例えば、以下のようにCFnテンプレートを作成すれば、終了保護がTrueになります。

マネコンで作成した起動テンプレートを指定してEC2を作成する場合
AWSTemplateFormatVersion: "2010-09-09"
Description: "Overwriting test of template settings to be imported"
Resources:
  EC2Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      LaunchTemplate:
        LaunchTemplateId: "lt-xxxxxxxxxxxxxxxxx"
        Version: "1"
      DisableApiTermination: true # 終了保護設定項目を追加
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【AWS】EBSまとめてみた

EBS(Elastic Block Storage)とは

EBSとはEC2にアタッチされるブロックレベルのストレージサービス

特徴

  • OSやアプリケーション、データの起きばなど様々な用途で利用される
  • EC2とはネットワーク接続されている
  • 99.999%の可用性を持ち、サイズは1G~16TBでサイズと利用期間で課金される
  • ボリュームデータはAZないで複数のHWにデフォルトでレプリケートされており、冗長化不要
  • データは永続的に利用可能
  • EC2インスタンスは他のAZ内のEBSにアクセスはできない
  • 1つのEBSを複数のインスタンスで共有することはできない
  • 同じAZのインスタンスのみ付け替えが可能

スナップショット

  • EBSはスナップショットにてバックアップ可能
  • スナップショットからEBSを復元する際は別AZ、別リージョンにも可能
  • スナップショットはS3に保存される
  • 増分バックアップ方式
  • スナップショットに容量で課金される

EBSのボリュームタイプ

EBSのボリュームタイプは大きくSSD型とHDD型、マグネティックの二つがある。それぞれ詳しくみていく。

汎用SSD

SSDをベースとした汎用的なボリュームタイプ。ユースケースとしては、
- 開発環境
- 小-中規模のデータベース
に使用される。
サイズは、1G~16TBまで保存可能。10000IOPS/ボリュームまで容量に応じたベースライン性能がある。1TB未満のボリュームには、一時的なIOPSの上昇にも対応できるようにバースト性能が用意されている。

プロビジョンドIOPS

EBSの中でも最も高性能なSSDをベースとしたボリュームタイプで、高いIOPS性能が求められる場合に利用する。ユースケースとしては、
- 高いIOPSを必要とするNoSQLやアプリ
- 大規模DB
に使用される。
最大64000IOPS/ボリュームまで容量に応じたベースライン性能がある。

スループット最適化HDD

HDDをベースとしたスループット重視のボリュームタイプユースケースとしては、
- ビックデータ処理
- 大規模なETL処理やログ分析
に使用される。
スループットを性能指標としており、1TB当たり40MB/秒、最大スループットはボリューム当たり500MB/秒のベースライン性能がある。

Cold HDD

最も低コストなボリュームタイプで利用頻度があまりなく、性能もあまり高くないボリュームタイプ。ユースケースとしては
- ログデータやアクセス頻度が低いデータ
- バックアップやアーカイブ
に使用される。

マグネティック

旧世代のボリュームで基本使用しない。

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

【AWS】EC2改めてまとめてみた

EC2とは

AWSが提供する仮想サーバ
特徴
- 起動・停止・削除・マシンスペック変更がボタン一つで数分で可能
- 使って分だけ課金される従量課金制
- インスタンスという単位でサーバが管理される
- 汎用的なIntelアーキテクチャを採用
- Window、Linuxなど多様なOSをサポート
- OSより上の層はユーザ自身でカスタマイズ可能
- 独自のAmazon Machine Image (AMI)にOS設定を作成し、保存して再利用が可能
- 数分で起動可能・スペック変更可能なため、必要に応じてインフラリソースを短時間で調達することが可能。

インスタンスタイプ

CPU、メモリ、ストレージ、ネットワークキャパシティーなどサーバーリソースのこと。100以上の中から自身が使用するワークロードに最適なインスタンスタイプを選択する。

インスタンスファミリー

メモリ、I/O、CPUクロック重視、GPU、FPGA搭載などの特徴を持ち大きく5つのカテゴリに分類される。
例) T2.micro
T:インスタンスファミリー
2:世代
micro:インスタンスサイズ

カテゴリ 該当するインスタンスファミリー
汎用 T、M
メモリ最適化 R、X
高速コンピューティング P、G、F
ストレージ最適化 I、D、H
コンピューティング最適化 C

インスタンスタイプの選び方の参考記事
https://pages.awscloud.com/rs/112-TZM-766/images/C2-07.pdf
業務でなぜこのインスタンスタイプを使用するのだろう?と思うことがあったので、この記事をみてなるほどと思いました。

インスタンスの利用形態

オンデマンドインスタンス

従量課金制の一般的なインスタンス利用形態

リザーブドインスタンス

長期間利用することを約束することで割引を受けることができるインスタンス利用形式。オンデマンドインスタンスと比べて最大75%割安になる。ユースケースとしては、使用量が一定または予測可能なワークロード、アプリケーションの場合に使用する。

スタンダード コンバーティブル
利用期間 1年(40%割引)/3年(60%割引) 1年(31%割引)/3年(54%割引)
AZ/インスタンスサイズ/ネットワークタイプ変更可否
インスタンスファミリー/OS/テナンシー/支払オプション変更可否 なし
売却可能か 可能 今後可能になる予定

スポットインスタンス

予備のコンピューティング容量を、オンデマンドインスタンスに比べて(最大90%)割引で利用できるインスタンス利用形式。一時的なオートスケール用、短時間のスペック増加に使うといい。
特徴
- 入札形式で利用するためとても安い
- 起動に通常よりも時間がかかる
- 予備用のため途中で削除される可能性がある。

Saving Plan

1〜3年の期間に一定の使用量を守ることによりコストを削減する
特徴
- 1年または3年の期間に特定の処理能力(USD/時間)を使用する契約を結ぶことで適用される割引契約
- AWSコンピューティング使用料金を最大72%節約できる
- EC2、Fargate、AWSLambdaで適用可能

物理対応可能なインスタンス

物理サーバにインスタンスを起動して制御が可能なタイプ

ハードウェア専用インスタンス

  • 専用HWのVPCで実行されるEC2インスタンス
  • ホストHWレベルで他のAWSアカウントに属するインスタンスから物理的に分離する
  • 同じAWSアカウントのインスタンスとは共有する可能性がある

Dedicated Host

  • 完全に専用として利用できる物理サーバ上でEC2インスタンスを稼働できる

Bare Metal

  • OSより下のレイヤーを操作できる

ストレージ

EC2で直接利用するストレージは不可分なインスタンスストアと自分で設定するEBSの2つがある

インスタンスストア

  • EC2と同じサーバーを使用する形式。EC2と不可分のブロックレベルの物理ストレージ
  • EC2の一時的なデータが保持され、停止、終了とともに削除される
  • 無料

Elastic Block Storage(EBS)

  • ネットワークで接続されたブロックレベルのストレージでEC2とは独立して管理される
  • ハードディスク代わりとして利用する
  • EC2を削除してもEBSは削除されない設定が可能で、snapshotをS3に保持可能
  • EBS使用料金がかかる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WSL2を使ったDokcerで、`Aws::S3::Errors::RequestTimeTooSkewed` のエラー対処法

環境

  • WSL2 (Windows 10 Home)
  • Docker for Windows

エラー

Docker上で、AWSと連携しようとしたときに、

Aws::S3::Errors::RequestTimeTooSkewed: The difference between the request time and the current time is too large.

のエラーが出た

原因

WSL2で動くDockerコンテナの時刻が大幅にずれているのが原因

Windows ホスト側

$ data 
Sun Aug 30 18:16:29 JST 2020

コンテナのシェル内

$ data
Sun Aug 28 11:16:29 JST 2020

解決法

はじめに

Aws::S3::Errors::RequestTimeTooSkewedのエラーはTimezoneが違う場合でも発生するので、
docker-compose.yml 内でtimezone をJSTに設定しとく。

docker-compose.yml
services:
  web: 
   environment:
     TZ: Asia/Tokyo

つぎに

WSL2の時刻を data --set コマンドで同期させる。
まず、コンテナのシェル内でdataコマンドを使うときに、 operation not permitted にならないように、権限設定をしとく必要がある。

docker-compose.yml
services:
  web:
   privileged: true

つぎに

コンテナのシェル内で、dataコマンドを使って日付を変更する

$ data --set "2020-08-30 18:16:29"

で完了。

他に試したこと

  1. hwclock -s を用いて無理やり時間を変えれるらしい。

-> 下記のエラーが出て、少し解決しようとしてみたが、謎すぎて諦めた

$ hwclock -s
hwclock: Cannot access the Hardware Clock via any known method.
hwclock: Use the --debug option to see the details of our search for an access method

参考リンク

https://qiita.com/sadakan5/items/4c0a394fd1d6be34efb2

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

[Python × AWS × Serverless] PyCon JP 2020 の登壇で言い残したこと

Python × AWS × Serverless 初学者が次の一歩を踏み出すためのテクニック

というタイトルで登壇してきました。社外の登壇は超久々でとても緊張しました。

当初考えてた内容から路線変更したりとか、人はなぜ締め切りをめいっぱい使ってしまうのか、とか、色々考えたりしながらなんとか形にはなりましたが、構成や個別のトピックとか思い返せばまだ言い足りないことがあったなと思いましたのでここで補足しようと思います。

スライドは SpeakerDeck で公開しています。

スライドタイトルについて

内容的に Tips の紹介という趣のスライドなので、「これで初学者を抜け出せるのか?」って疑問にはハッキリ回答できてないかもなぁ、、、と感じました。

もちろんタイトル詐欺の意図はありません。自分なりにこのタイトルと内容を選んだ背景が(一応は)ありますので記しておきます。

私のバックグラウンドですが、AWS方面は割と知見があるものの、アプリ開発のがっつりした経験は持っていない、といったスキルセットです。このような背景があって、基本的なディレクトリ構成とか、どうやって設計していくかとか、そのへんの慣れがなく戸惑っていた側面が大きかったんです。で、わからないがゆえに「サーバーレスに固有な何かがあるのでは?」みたいな不安も出てきて、どうするのが良いのかわからなかったです(自覚的ではなかったのでなかなか解消できませんでしたが)。

その後、(本発表でも喋ったように)サーバーレスといえどアプリ開発部分の考え方は普通に言語のプラクティスに従えば良いとわかりました。これからサーバーレスに挑戦される皆さまには、必要以上に恐れることないよということが伝わればいいなと。

私自身も、以前はサーバーレスについて「次何したらいいのかわからなくてモヤモヤする」状態を経験していました。そのときに知りたかった情報というのが今回のネタだった、ということで発表に至っております。

このような前置きの共有って地味に大事な部分ですよね。共感を得られるストーリーとか Why が提起されないプレゼンって訴求力がないので。ここを省いてしまったことで、視聴者のみなさまに訴求するメッセージがわかりにくくなってしまったかもしれないなと、大いに反省しました。

AWS サービスのリソース名を明示的に与える

「ステージ」云々のセクションで説明を入れそびれた部分です。

Serverless Framework は、デプロイするAWSリソースの名前をステージ間で衝突しないようによしなに命名してくれます。よって、 Serverless Framework を使っていれば多くの場合 Name 属性は必須ではありません。

しかし、あえて「リソース名は明示的に命名した方がいいんじゃないかな?」というのがここで述べたいことです。スライドにも実はちょろっと出てきますが、命名規則というのは例えばこういうものです↓

# template.yml
# SQS Queue name
Name: ${self:service}-my-queue-${self:provider.stage}

こういう命名にすることでサービス間/ステージ間で名前が被らない・予測しやすいリソース名管理ができます。

反対意見として、「インフラは全部コードで管理していくんだから、人間が見るための名前に気を遣う必要なくない?」とする考え方もあります。私もどっちかと言えばそっちを支持したい派です。

リソース名の衝突や、文字数制限 (例: lambda の関数名は64文字まで) などの問題を気にしなくて済む分、自分で変に命名しない方が楽だったりします。命名規則の管理も、(スタックの)ソースの記述が増えてうるさくなっちゃいいますし。できるなら名前はおまかせの方が嬉しいんです。

では、何故明示的な命名をした方がよいのでしょうか。個人的にうれしいポイントは2つあって、それぞれ以下の通りです。このへんは自分が受け持ってる案件の状況がバイアスに入ってると思います。たぶん。

Lambda のログを検索しやすい

Step Functions をよく利用するので、複数の Lambda にまたがってログを検索したいシーンがちょいちょい出てきます。

ログの検索にはいまのところ CloudWatch Logs Insights を使うケースがほとんどなのですが、その時のロググループの名前が(明示的に命名規則を付けていると)スッキリしますので、検索がちょっとだけやりやすいです。

おそらく、Datadogを利用しつつログ検索はタグで絞るようにするとか、(命名規則に頼らずとも)色々やりようはあるんだろうと思うのですが、今自分が採れる選択の中ではこのやり方が運用シーンにも即しておりそこそこ妥当性のあるやり方かなと思っています。

※このへんは周辺ツール (CLI や Datadog など)をもうちょっと充実させれば景色も見解も変わってくると思います

リソース名を参照する時に見通しがよく、保守しやすい

スタックの記述とアプリ側の記述、どっちもある程度スッキリ書くためには命名規則はあった方がメリットが大きいと思います(より主張したいメリットはこっちです)。

シンプルな構成図なら CloudFormation の GetAtt やら Ref やらで値を引いてやればいいので、さほど問題じゃありません。命名規則が役立つのは、構成図が膨れてきた頃合いです。

例えば、SQS のキューを複数作っている構成を想像してみます。Python のコード上ではキューのエンドポイント、すなわち Queue URL (AccountId, Region, QueueName の3つによって特定可能) が必要です。

作成したキューの URL を Ref で参照して lambda 環境変数に渡してあげてもよいのですが、個人的にはアカウントID、リージョン、キューの Name 属性をそれぞれ渡してあげれば十分かな、と思います。

アカウントIDやリージョンは他のAWSサービスを利用する場合にも出番がありそうです。あって困るものではなさそうですし、Queue URL を組み立てるユーティリティ関数の実装もごく簡単ですので、小回りが効きそうな方を選択するのが良いだろうと考えています。

Python のコードベースとしての管理は上記の通りとして、今度はスタック側の管理を考えてみます(どちらかと言えば、命名規則のメリットが大きいのはこちらだと考えます)。

さっきの SQS のキュー名の定義を再掲します。キューの名前は、 servicestage を join して作るイメージでした。

# 【再掲】 template.yml
# SQS Queue name
Name: ${self:service}-my-queue-${self:provider.stage}

実際のスタック定義としては、上記の Name の文字列全体(あるいは my-queue の部分)をテンプレートの環境変数 or custom セクションで宣言してあげる感じになります。

このやり方をすることで、他所のリソース(例えば IAM ポリシーの定義)からの参照が比較的スッと書けます。名前の部分は変数として参照可能な宣言を行っているので、 ARN などは単に join して組み立てるだけです。

ここで「いやいや、 Ref とか GetAtt とかあるじゃん」という指摘があると思いますが、それらを迂闊に使うのは個人的にあまりおすすめできません。

特にポリシー周りにおいて、これらを濫用すると CloudFormation の循環参照が起こりやすい気がします。このへんは経験則によるところが大きく明確な文章で根拠を示せませんが、私としては軽々しく AWS リソースを直接参照する関数を使いたくないんですよね...。代えて、依存先として害が少ない環境変数や、 custom セクションの変数に依存させる戦略を採っています。意識的に(特にIAMポリシー周りでの) Ref, GetAtt は使用を控えめにしています。

ただでさえ CloudFormation のデバッグは苦行なのに、構成が大きくなってから後で CloudFormation の循環参照問題に頭抱えるハメになるの、イヤですよね...? あれは人類がデバッグするものじゃないです...

このような理由があって、私はリソース名を(多少冗長で面倒であったとしても)明示的に宣言するのが好みです。この節で書いたことはあくまで副次的な効果ではありますが、私としては結構重く見ています。

おわり

サーバーレスのネタって、概要をさらう資料やチュートリアルをこなす程度の情報なら割とそのへんに転がってるのですが、結局それだけでは実際の開発シーンにおいて痒い部分を解消してくれない実感があったので、そのへんを埋めたい思いで応募してみました。

今の私が伝えたいことはおおよそ喋れたと思いますが、AWS周りの基礎知識はもっと深堀りできる部分だったので45分セッションで応募してみても良かったかもしれません(Python 成分が薄くなっちゃうので TPO 的にどうなの、とは思います)

一方で、残念ながら思ったより反応が少なめだったので、今回の話自体に需要があったのかどうか、今回の発表からはいまひとつ感触がつかめないところではありました(裏番組のセッションが強すぎた点、私の発表自体も改善余地があった点、PyCon の主要な参加層にはミートしづらいネタだった、など考えうる要因は複数あります)。需要皆無ということはおそらくないはずなので、場を変えて喋ってみるなどして探っていこうと思います。ベースの資料はできたことですし。

実践寄りな知識をもうちょっと体系立てて書けたら良いなとも思います。次回の技術書典で頑張ってみるというのも良いかもしれませんね。

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

[初心者向け] 常に小さくテストすることを心がけよう

概要

コードを書いていて、思うように動かない。どう書けばいいかわからない。困った。
そういうことはないだろうか?

試行錯誤を繰り返して動作確認する必要があることは時々ある。そんなときに、大きなproductionコードを動かしながらtry and errorしていたりしないだろうか?

「時間がないから」と言い訳し別の方法を検討する事を拒否して、production上で何度も何度も実行しながら「遅い!」と文句を言っていないだろうか?

大きなコードで動作確認を繰り返すと無駄に時間を消費するし、それだけでなく確認したいそもそもの対象以外から影響を受けて、次第に自分が何を確認しているか分からなくなってくる。

【動作確認は可能な限り絞ったサンプルを使ってテストするべきだ。】

【言語にシェル環境があれば、シェル環境を利用することを勧める。】

そんな話をしたい。

あるケースの場合

あるバッチ処理で、大きなcsvファイルをpythonで処理してs3に格納する必要があった。
そのため、最終的にpandasが持つ大きなデータをboto3を用いてアップロードする方法が問題になった。

オリジナルコード

s3 = boto3.client('s3');
// df : pandas.DataFrame
s3.put_object(df.to_csv().encode(), bucket, key)

この場合、to_csv()が全データを文字列化して出力し、さらにそれをencode()が
bytes配列化して出力する。つまり、dfが持つデータが巨大になるとその数倍の速度でメモリを食いつぶしていた。

これをstream処理にしたい。

stream処理?

巨大なデータを全てオンメモリに展開するのではなく、一部ずつ処理していく事によりメモリ消費量を減らすことができる。一般的にこのような処理をstream処理と呼ぶ。

pythonのstream処理

stream処理を行う方法には、generatorを使う方法とioパッケージを使用する方法がある。

データストリーム

https://docs.python.org/3/library/io.html

ioパッケージには、io.BytesIOやio.StringIOというオンメモリストリーム処理をするためのクラスが用意されている。

class 意味
io.BytesIO バイトストリーム
io.StringIO 文字列ストリーム
io.TextIOWrapper 文字列IOラッパー(バイトストリームを文字列ストリーム化する)

このあたりのクラスを使用する。

論理処理(クラスなど)のストリーム

generatorは、多くの論理ブロックを順次処理するときに用いる。
例えば大量の行を持つcsvを、一行ずつ処理したい場合、全ての行を一度に読み込むのではなくブロックごとに読みながら処理できる。

generatorをここでは説明しないが、pythonで初心者を脱するためにはgeneratorの知識は必須だ。

実際にstream化してみよう!

今回やりたいのはデータ処理のストリーム化なので、ioパッケージの話。

$ python
Python 3.6.8 (default, Dec  5 2019, 17:44:50)
[GCC 7.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

コマンドラインでpythonと打ち込むと、pythonシェルが起動する。
ここで調査を行う。

>>> import pandas
>>> import boto3
>>> import io

今回動作確認に利用するパッケージはこの3つ。
まず検索したところ、以下で動きそうな雰囲気だった。

>>> df = pandas.DataFrame([1,2,3])
>>> sio = io.StringIO()
>>> df.to_csv(sio)
>>> s3 = boto3.client('s3')
>>> s3.upload_fileobj(sio, backet, key)

s3.upload_fileobjはs3でstreamingにアップロードするための関数らしい。
実行したところ、s3上にファイルはできていたが、ファイルのサイズは0だった。
調べたところ、upload_fileobjがbyte streamでなければならないようだ。

そこで、StringIOではなくBytesIOを使ってみる。

>>> bio = io.BytesIO()
>>> df.to_csv(bio)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/kishibashi/pandas/pandas/core/generic.py", line 3167, in to_csv
    formatter.save()
  File "/home/kishibashi/pandas/pandas/io/formats/csvs.py", line 206, in save
    self._save()
  File "/home/kishibashi/pandas/pandas/io/formats/csvs.py", line 314, in _save
    self._save_header()
  File "/home/kishibashi/pandas/pandas/io/formats/csvs.py", line 283, in _save_header
    writer.writerow(encoded_labels)
TypeError: a bytes-like object is required, not 'str'

to_csvがBytesIOにstrを渡したために、BytesIOが怒った。
つまり、to_csvからテキストストリームで受け取りつつ、upload_fileobjにバイトストリームで渡す必要がある。

このようなストリームの変換に使うのがio.TextIOWrapperというクラスだ。使ってみよう。

>>> bio = io.BytesIO()
>>> w = TextIOWrapper(bio)

bioやwにwriteしたりreadしながらどう動くのか確認して以下の事がわかった。
TextIOWrapperはBytesIOをラップしてくれて、StringIOと同じ機能をもつ。そして、bioとwは入力と出力のような方向性をもっていない。bioにbytesを書き込むとbioからもwからも読み出せるし、wにテキストを書き込むとwからもbioからも読み出せる。

つまりこうなる。

>>> bio = io.BytesIO()
>>> w = TextIOWrapper(bio)
>>> df.to_csv(w)
>>> w.seek(0)
>>> s3.upload_fileobj(bio, bucket, key)

これでs3上のファイルにデータがアップロードされることが確認できた。

ちなみにseekは読み始める位置を最初に変更するおまじないだ。

伝えたい事

この文章で伝えたい事は、ここに書いてあるstreaming処理の詳細ではない。

【分からないことは動作確認するのが理解の早道であり、動作確認はできるだけ小さくしよう】

ということ。
上記のpython shell上の動作確認では、pandas、io、boto3以外なにもない。
どこからpandasに与えるcsvデータを持ってくるのか、サーバーがdjangoで動いているかどうかなど、productionにありがちな外部環境は、この動作確認には全く関係のない事だ。

  • shell上での確認
  • 単体テストを書いて確認
  • jupyter notebookのような環境に慣れているならそちらでも

プロダクションコード上での動作確認は時間の無駄なので絶対にしないように。
(もちろん、少し修正すれば動く事が分かっているならその限りではない)

他の言語のシェル環境は?

JavaScript

$ node
Welcome to Node.js v12.13.1.
Type ".help" for more information.
>

Golang

標準では用意されていない模様。

Java

JDK9からJShellというモノが追加されている模様。使ったことはない。

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

Rails 6 で omniauth-cognito-idp を使って Amazon Cognito 認証を実装するサンプル

要点

Amazon Cognitoを使えば、deviseに依存せずに、ユーザ登録・ログイン・ログアウト・パスワードリセット・ソーシャルログイン・n段階認証・SAMLなど、様々な機能がアプリケーションコードから分離するかたちで追加可能です。Amazon Cognitoの他にも、Auth0やFirebase Authentication等があり、IDaaS(Identity as a Service)と呼ばれます。

本記事では、Railsアプリケーションで動作検証をする場合の手順を記載しています。

  1. Amazon Cognitoのユーザープールの作成とクライアントの設定の基本的な手順を解説(作業時間: 10分程度)
  2. 動作検証するためのサンプルアプリの実装方法を解決(作業時間: 10分程度)

なお、記事中のスクリーンショットに記載されたクライアントIDやシークレットは、既に削除されているため利用することはできません。ご自身で設定して動作検証してみてください。

環境

% ruby -v
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-linux]

% bin/rails version
Rails 6.0.3.2

手順1: ユーザープールの作成とクライアントの設定

作業時間: 10分程度

Amazon Cognito のコンソールから「ユーザープールを作成する」を押下

Screenshot from 2020-08-30 12-33-26

プール名を入力

サンプルなのでデフォルトを確認するを押下

Screenshot from 2020-08-30 11-48-21

プールの作成

Screenshot from 2020-08-30 11-51-25

作成完了

プール ID をメモする:

Screenshot from 2020-08-30 11-53-53

ドメイン名を設定

ナビゲーションの ドメイン名 をクリックし、認証フォームのURLを設定。設定した、 https://{your prefix}.auth.ap-northeast-1.amazoncognito.com をメモする:

Screenshot from 2020-08-30 11-55-48

アプリクライアントの作成

ナビゲーションの アプリクライアント をクリックし、 アプリクライアントの追加 をクリック:

Screenshot from 2020-08-30 12-00-18

Screenshot from 2020-08-30 12-01-25

アプリクライアントIDアプリクライアントのシークレット をメモ:

Screenshot from 2020-08-30 12-02-23

アプリクライアントの設定

ナビゲーションの アプリクライアントの設定を押下し、コールバックURLとサインアウトURLを設定(それ以外の設定はスクリーンショットを参照):

Screenshot from 2020-08-30 12-04-49

これでcognito側の設定は完了です(なお、スクリーンショットの値は利用できません)。

手順2: アプリケーションの実装

作業時間: 10分程度

rails new

サンプルなので割愛

omniauth-cognito-idp を追加

gem 'omniauth-cognito-idp'

クレデンシャルの追加

今回はサンプルなので development で、ユーザプール作成時に生成した諸々をセットする:

$ bin/rails credentials:edit -e development
aws:
  access_key_id: 123
  secret_access_key: 345
  # 上記でメモした「ドメイン名」に含まれるリージョン
  region: ap-northeast-1
  # 上記でメモした「アプリクライアントID」
  cognito_client_id: *****YOUR CLIENT ID*****
  # 上記でメモした「アプリクライアントのシークレット」
  cognito_client_secret: *****YOUR SECRET*****
  # 上記でメモした「ドメイン名」
  cognito_user_pool_site: https://*****YOUR PREFIX*****.auth.ap-northeast-1.amazoncognito.com
  # 上記でメモした「プールID」
  cognito_user_pool_id: *****YOUR POOL ID*****

イニシャライザの追加

# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider(
    :cognito_idp,
    Rails.application.credentials.aws[:cognito_client_id],
    Rails.application.credentials.aws[:cognito_client_secret],
    client_options: {
      site: Rails.application.credentials.aws[:cognito_user_pool_site]
    },
    scope: 'email openid',
    user_pool_id: Rails.application.credentials.aws[:cognito_user_pool_id],
    aws_region: Rails.application.credentials.aws[:region],
  )
end

HomeControllerの実装

root で表示する画面:

$ bin/rails g controller home show --skip-assets --skip-helper
<h1>Home#show</h1>
<p>Find me in app/views/home/show.html.erb</p>

<h1>RoR Cognito Sample</h1>
<p>Step 1 - Login.</p>
<%= button_to 'Login', 'auth/cognito-idp', method: :post %>
# config/routes.rb
Rails.application.routes.draw do
  root 'home#show'
end

CognitoIdpControllerの実装

OmniAuthで認証後の処理をするコールバック:

$ bin/rails g controller cognito_idp --skip-template-engine --skip-assets --skip-helper
class CognitoIdpController < ApplicationController
  def callback
    # 実際は provider と uid を使って、Userレコードを作成/参照し、user_id をsessionにセットするが
    # サンプルなのでauth hashをセット
    session[:userinfo] = request.env['omniauth.auth']

    redirect_to '/dashboard'
  end
end
# config/routes.rb
Rails.application.routes.draw do
  root 'home#show'
  get 'auth/cognito-idp/callback' => 'cognito_idp#callback'
end

セッションストアでCookieを利用して session[:userinfo] をセットしようとすると最大容量を超過するので、セッションストアをメモリに変更:

# config/initializers/session_store.rb
Rails.application.config.session_store :cache_store

config/environments/development.rbconfig.cache_store = :memory_store を追加。

ApplicationControllerに認証に関する振る舞いの追加:

実際はcurrent_userなども実装するがサンプルなのでuserinfoがsessionにセットされているかどうかを確認:

class ApplicationController < ActionController::Base
  private

  def authenticate_user!
    return redirect_to(root_path) unless signed_in?
  end

  def signed_in?
    session[:userinfo].present?
  end

  helper_method :signed_in?
end

DashboardControllerの実装

ログイン後の画面を実装する:

$ bin/rails g controller dashboard show --skip-assets --skip-helper
class DashboardController < ApplicationController
  before_action :authenticate_user!

  def show
  end
end
# config/routes.rb
Rails.application.routes.draw do
  root 'home#show'
  get 'auth/cognito-idp/callback' => 'cognito_idp#callback'
  get 'dashboard' => 'dashboard#show'
end
<h1>Dashboard#show</h1>
<p>Find me in app/views/dashboard/show.html.erb</p>
<p><%= session[:userinfo].inspect %></p>

<p><%= link_to 'logout', logout_path, method: :delete %></p>

SessionsControllerの実装:

ログアウト処理を実装(cognito側もログアウトする必要がある):

$ bin/rails g controller sessions --skip-template-engine --skip-assets --skip-helper
class SessionsController < ApplicationController
  before_action :authenticate_user!

  def destroy
    reset_session
    redirect_to cognito_logout_url
  end
end

direct ルーティングを使って、cognito側のログアウトを実装:

# config/routes.rb
Rails.application.routes.draw do
  root 'home#show'
  get 'auth/cognito-idp/callback' => 'cognito_idp#callback'
  get 'dashboard' => 'dashboard#show'
  delete 'logout' => 'sessions#destroy'

  direct :cognito_logout do
    query = {
      client_id: Rails.application.credentials.aws[:cognito_client_id],
      logout_uri: root_url,
    }.to_param
    "#{Rails.application.credentials.aws[:cognito_user_pool_site]}/logout?#{query}"
  end
end

動作検証

rails server を起動して localhost:3000 にアクセス:

ルートを表示

Login を押下:

Screenshot from 2020-08-30 12-11-06

cognitoのログインフォームが表示されるのでサインアップする

Screenshot from 2020-08-30 12-13-05

Screenshot from 2020-08-30 12-14-22

メールアドレスを確認して認証コードを入力するとログインが完了します。

Screenshot from 2020-08-30 12-16-44

参考

以上

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

Amazon ECRがOCI ArtifactをサポートしてHelm chartsも管理できるようになったよ

はじめに

2020/8/28 に Amazon ECR が OCI Artifact をサポートしました。
これにより Helm charts や OPA Bundles など、コンテナイメージ以外の
コンテンツタイプ (アーティファクト) を ECR で保存、配布できるようになります。

OCI Artifact Support In Amazon ECR
https://aws.amazon.com/jp/blogs/containers/oci-artifact-support-in-amazon-ecr/

例えば組織内で安全に Helm charts を共有しようとした場合に
これまでは ChartMuseum 等を使用して、プライベートな
Helm chart registry を構築する必要がありましたが、ECR だけで管理できるようになります。

8/17の AWS Container Day at KubeCon で Developer Preview として発表されたばかりだった
認識ですが、2週間もたたずに正式リリース?となったようです。
今回のローンチの対象は SDK と CLI のサポートで、マネジメントコンソール側の変更は
今年後半にリリース予定とのこと。

ほぼドキュメント通りの手順になってしまいますが、やってみます。

作業環境

  • Helm: v3.3.0
  • AWS CLI: 2.0.43
  • Kubernetes: 1.16.5 (Docker Desktop)

Helm のインストール

クライアント端末に Helm 3 がインストールされていない場合はインストールしておきます。
Github のリリースページからバイナリを取得、インストールスクリプトの使用、
パッケージマネージャー経由など複数のインストール方法が準備されています。

Installing Helm
https://helm.sh/docs/intro/install/

$ helm version
version.BuildInfo{Version:"v3.3.0", GitCommit:"8a4aeec08d67a7b84472007529e8097ec3742105", GitTreeState:"dirty", GoVersion:"go1.14.7"}

Helm 3 における OCI のサポートは 現在 experimental な機能とみなされているため
HELM_EXPERIMENTAL_OCI を環境変数に設定する必要があります。

export HELM_EXPERIMENTAL_OCI=1

ECR レポジトリの作成と認証

AWS CLI で作成します。
特に追加のオプション等は不要で、いつもどおり作成できます。

$ aws ecr create-repository --repository-name helm-test
{
    "repository": {
        "repositoryArn": "arn:aws:ecr:ap-northeast-1:123456789012:repository/helm-test",
        "registryId": "123456789012",
        "repositoryName": "helm-test",
        "repositoryUri": "123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/helm-test",
        "createdAt": "2020-08-29T12:22:02+09:00",
        "imageTagMutability": "MUTABLE",
        "imageScanningConfiguration": {
            "scanOnPush": false
        },
        "encryptionConfiguration": {
            "encryptionType": "AES256"
        }
    }
}

以下のコマンドで Helm クライアントを ECR に認証します。

$ aws ecr get-login-password | helm registry login \
> --username AWS --password-stdin 123456789012.dkr.ecr.region.amazonaws.com
Login succeeded

aws ecr get-login コマンドは非推奨になっており、AWS CLI v2 では削除されているため
aws ecr get-login-password を使用します。

参考:AWS CLIでECRにログインする時はget-loginではなくget-login-passwordを使おう
https://qiita.com/hayao_k/items/3e4c822425b7b72e7fd0

Chart の作成

helm create コマンドで Chart の雛形を作成することができます。

$ mkdir helm-test 
$ cd helm-test

$ helm create mychart
Creating mychart

今回はこの雛形をそのまま利用します。

ECR へ Push

helm chart save コマンドで Chart ディレクトリをローカルキャッシュに保存します。
この際、リモートの ECR の URI を使用して、エイリアスを作成します。
このあたりは、コンテナイメージを push するときと同じ感覚です。

$ helm chart save mychart mychart
ref:     mychart:0.1.0
digest:  c455f9c99430ee099ea66999a40a0978947ccfd76e6922cdb8a911491326b0ed
size:    3.5 KiB
name:    mychart
version: 0.1.0
0.1.0: saved

$ helm chart save mychart 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/helm-test:mychart
ref:     123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/helm-test:mychart
digest:  4865989e73df2c14b15ee2a1befa11673e455cc5a728239a05f5bc12b13cadb3
size:    3.5 KiB
name:    mychart
version: 0.1.0
mychart: saved

$ helm chart list
REF                                                             NAME    VERSION DIGEST  SIZE    CREATED
123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/helm-te...    mychart 0.1.0   4865989 3.5 KiB 28 seconds
mychart:0.1.0                                                   mychart 0.1.0   4865989 3.5 KiB About a minute

helm chart push コマンドで ECR に push できます。

$ helm chart push 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/helm-test:mychart
The push refers to repository [123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/helm-test]
ref:     123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/helm-test:mychart
digest:  4865989e73df2c14b15ee2a1befa11673e455cc5a728239a05f5bc12b13cadb3
size:    3.5 KiB
name:    mychart
version: 0.1.0
mychart: pushed to remote (1 layer, 3.5 KiB total)

今回のリリースではマネジメントコンソールに変更が入っていないため、
現在は普通に Docker イメージが格納されているだけのように見えます。

image.png

push した Chart を確認すると、適切な artifactMediaType で保存されていることがわかります。

$ aws ecr batch-get-image --repository-name helm-test --image-ids imageTag=mychart --query 'images[].imageManifest'
[
    "{\"schemaVersion\":2,\"config\":{\"mediaType\":\"application/vnd.cncf.helm.config.v1+json\",\"digest\":\"sha256:65a07b841ece031e6d0ec5eb948eacb17aa6d7294cdeb01d5348e86242951487\",\"size\":141},\"layers\":[{\"mediaType\":\"application/tar+gzip\",\"digest\":\"sha256:acd033c1b722f7dcbfb38a909c422540c584feef52ad4aedb8289eb074638f27\",\"size\":3562}]}"
]

ECR から Chart を pull する

pull を確認するため、helm chart remove コマンドで ローカルキャッシュ上の Chart を削除します。

$ helm chart remove 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/helm-test:mychart
mychart: removed

$ helm chart remove mychart:0.1.0
0.1.0: removed

helm chart pull コマンドで pull します。

$ helm chart pull 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/helm-test:mychart
mychart: Pulling from 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/helm-test
ref:     123456789012.ecr.ap-northeast-1.amazonaws.com/helm-test:mychart
digest:  4865989e73df2c14b15ee2a1befa11673e455cc5a728239a05f5bc12b13cadb3
size:    3.5 KiB
name:    mychart
version: 0.1.0
Status: Downloaded newer chart for 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/helm-test:mychart

$ helm chart list
REF                                                             NAME    VERSION DIGEST  SIZE    CREATED
123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/helm-te...    mychart 0.1.0   4865989 3.5 KiB 9 hours

Chart のデプロイ

pull した Chart を使用して Kubernetes にデプロイするには
helm chart export コマンドを使用して一度ローカルに export する必要があります。

helm chart export 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/helm-test:mychart --destination ./chart-install
ref:     123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/helm-test:mychart
digest:  4865989e73df2c14b15ee2a1befa11673e455cc5a728239a05f5bc12b13cadb3
size:    3.5 KiB
name:    mychart
version: 0.1.0
Exported chart to chart-install\mychart/

helm install コマンドで Namespce: helm-test に Chart をインストールします。

$ cd chart-install
$ kubectl create ns helm-test
namespace/helm-test created

$ helm install mychart ./mychart -n helm-test
NAME: mychart
LAST DEPLOYED: Sun Aug 30 11:28:24 2020
NAMESPACE: helm-test
STATUS: deployed
REVISION: 1
NOTES:
1. Get the application URL by running these commands:
  export POD_NAME=$(kubectl get pods --namespace helm-test -l "app.kubernetes.io/name=mychart,app.kubernetes.io/instance=mychart" -o jsonpath="{.items[0].metadata.name}")
  echo "Visit http://127.0.0.1:8080 to use your application"
  kubectl --namespace helm-test port-forward $POD_NAME 8080:80

問題なくデプロイできました。

$ kubectl get all -n helm-test
NAME                           READY   STATUS    RESTARTS   AGE
pod/mychart-5965fbff94-xxnd2   1/1     Running   0          32s

NAME              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/mychart   ClusterIP   10.99.155.110   <none>        80/TCP    32s

NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/mychart   1/1     1            1           32s

NAME                                 DESIRED   CURRENT   READY   AGE
replicaset.apps/mychart-5965fbff94   1         1         1       32s

$ helm list -n helm-test
NAME    NAMESPACE       REVISION        UPDATED                                 STATUS          CHART           APP VERSION
mychart helm-test       1               2020-08-30 11:28:24.6691172 +0900 JST   deployed        mychart-0.1.0   1.16.0

削除は helm uninstall コマンドを使用します。

$ helm uninstall mychart -n helm-test
release "mychart" uninstalled

$ kubectl delete ns helm-test
namespace "helm-test" deleted

Chart を ECR から削除する

ECR 上の Chart を削除する手順は、Docker イメージの時と変わりません。

$ aws ecr batch-delete-image --repository-name helm-test --image-ids imageTag=mychart
{
    "imageIds": [
        {
            "imageDigest": "sha256:4865989e73df2c14b15ee2a1befa11673e455cc5a728239a05f5bc12b13cadb3",
            "imageTag": "mychart"
        }
    ],
    "failures": []
}

コンソールから削除を行うことも可能です。
image.png

参考

Pushing a Helm chart
https://docs.aws.amazon.com/AmazonECR/latest/userguide/push-oci-artifact.html

以上です。
参考になれば幸いです。

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

AWS IoT CoreとAzure IoT Hubを両方やってみる

AWS_CLI-v2.0.36 Azure_CLI-v2.11.0
AWS IoT CoreとAzure IoT HubでIoT機器からデータをアップロードするサンプルを試しました。

手順の比較

No. AWS IoT Core Azure IoT Hub
(事前) - Azure CLIにエクステンションを追加する
(事前) - リソース グループを作成する
1 - IoT Hubを作成する
2 モノを作成する デバイスを作成する
3 証明書を作成する -
4 ポリシーを作成する -
5 証明書にポリシーをアタッチする -
6 証明書にモノをアタッチする -
7 送信側のプログラムを作成する 送信側のプログラムを作成する
  • AzureはIoT Hubそのものを作成する手順があります。
  • 認証方式がAWSはX.509 証明書、Azureは対称キーです。他の認証方式を使うと手順も変わると思います。

AWSの手順

モノを作成する

コマンド一発です。

aws iot create-thing --thing-name Thing001

証明書を作成する

パラメータで出力ファイル名を指定しています。

aws iot create-keys-and-certificate --certificate-pem-outfile "Cert001.cert.pem" --public-key-outfile "Cert001.public.key" --private-key-outfile "Cert001.private.key" --set-as-active

この先の手順で、実行結果に含まれる証明書のARN(certificateArn)が必要となりますのでメモっておきましょう。(下の実行結果はAWS CLIのリファレンスの例です)

実行結果
実行結果
{
    "certificateArn": "arn:aws:iot:us-west-2:123456789012:cert/9894ba17925e663f1d29c23af4582b8e3b7619c31f3fbd93adcb51ae54b83dc2",
    "certificateId": "9894ba17925e663f1d29c23af4582b8e3b7619c31f3fbd93adcb51ae54b83dc2",
    "certificatePem": "
-----BEGIN CERTIFICATE-----
MIICiTCCEXAMPLE6m7oRw0uXOjANBgkqhkiG9w0BAQUFADCBiDELMAkGA1UEBhMC
VVMxCzAJBgNVBAgEXAMPLEAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZBbWF6
b24xFDASBgNVBAsTC0lBTSEXAMPLE2xlMRIwEAYDVQQDEwlUZXN0Q2lsYWMxHzAd
BgkqhkiG9w0BCQEWEG5vb25lQGFtYEXAMPLEb20wHhcNMTEwNDI1MjA0NTIxWhcN
MTIwNDI0MjA0NTIxWjCBiDELMAkGA1UEBhMCEXAMPLEJBgNVBAgTAldBMRAwDgYD
VQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZBbWF6b24xFDAEXAMPLEsTC0lBTSBDb25z
b2xlMRIwEAYDVQQDEwlUZXN0Q2lsYWMxHzAdBgkqhkiG9w0BCQEXAMPLE25lQGFt
YXpvbi5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMaK0dn+aEXAMPLE
EXAMPLEfEvySWtC2XADZ4nB+BLYgVIk60CpiwsZ3G93vUEIO3IyNoH/f0wYK8m9T
rDHudUZEXAMPLELG5M43q7Wgc/MbQITxOUSQv7c7ugFFDzQGBzZswY6786m86gpE
Ibb3OhjZnzcvQAEXAMPLEWIMm2nrAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAtCu4
nUhVVxYUntneD9+h8Mg9qEXAMPLEyExzyLwaxlAoo7TJHidbtS4J5iNmZgXL0Fkb
FFBjvSfpJIlJ00zbhNYS5f6GuoEDEXAMPLEBHjJnyp378OD8uTs7fLvjx79LjSTb
NYiytVbZPQUQ5Yaxu2jXnimvw3rrszlaEXAMPLE=
-----END CERTIFICATE-----\n",
    "keyPair": {
        "PublicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkEXAMPLEQEFAAOCAQ8AMIIBCgKCAQEAEXAMPLE1nnyJwKSMHw4h\nMMEXAMPLEuuN/dMAS3fyce8DW/4+EXAMPLEyjmoF/YVF/gHr99VEEXAMPLE5VF13\n59VK7cEXAMPLE67GK+y+jikqXOgHh/xJTwo+sGpWEXAMPLEDz18xOd2ka4tCzuWEXAMPLEahJbYkCPUBSU8opVkR7qkEXAMPLE1DR6sx2HocliOOLtu6Fkw91swQWEXAMPLE\GB3ZPrNh0PzQYvjUStZeccyNCx2EXAMPLEvp9mQOUXP6plfgxwKRX2fEXAMPLEDa\nhJLXkX3rHU2xbxJSq7D+XEXAMPLEcw+LyFhI5mgFRl88eGdsAEXAMPLElnI9EesG\nFQIDAQAB\n-----END PUBLIC KEY-----\n",
        "PrivateKey": "-----BEGIN RSA PRIVATE KEY-----\nkey omittted for security reasons\n-----END RSA PRIVATE KEY-----\n"
    }
}

ポリシーを作成する

まずポリシードキュメントを作成します。お試しなのでフルオープンですのでお気をつけください。

policy.json
{
  "Version": "2012-10-17",
  "Statement": [
      {
          "Effect": "Allow",
          "Action": [
              "iot:*"
          ],
          "Resource": [
              "*"
          ]
      }
    ]
}

次にポリシーを作成します。

aws iot create-policy --policy-name Policy001 --policy-document file://policy.json

証明書にポリシーをアタッチする

コマンド一発。証明書のARNは先の手順で取得したものです。

aws iot attach-policy --policy-name Policy001 --target "{証明書のARN}"

証明書にモノをアタッチする

こちらもコマンド一発。証明書のARNは先の手順で取得したものです。

aws iot attach-thing-principal --thing-name Thing001 --principal "{証明書のARN}"

送信側のプログラムを作成する

https://aws.amazon.com/jp/premiumsupport/knowledge-center/iot-core-publish-mqtt-messages-python/
にあるサンプルを使います。

AWS IoT SDK for Python v2のインストール

pip install awsiotsdk

ソースコードの変更

変数名 内容
ENDPOINT *1で取得できます
CLIENT_ID モノの名前(Thing001)
PATH_TO_CERT CERTのファイルパス(Cert001.cert.pem)
PATH_TO_KEY プライベートキーのファイルパス(Cert001.private.key)
PATH_TO_ROOT ここのAmazonルートCA 1をダウンロード

*1 aws iot describe-endpoint --endpoint-type iot:Data-ATSコマンド

実行結果

20回送信したら終了します。

Connecting to xxxxxxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com with client ID 'Thing001'...
Connected!
Begin Publish
Published: '{"message": "Hello World [1]"}' to the topic: 'test/testing'
Published: '{"message": "Hello World [2]"}' to the topic: 'test/testing'
Published: '{"message": "Hello World [3]"}' to the topic: 'test/testing'
Published: '{"message": "Hello World [4]"}' to the topic: 'test/testing'
Published: '{"message": "Hello World [5]"}' to the topic: 'test/testing'
Published: '{"message": "Hello World [6]"}' to the topic: 'test/testing'
Published: '{"message": "Hello World [7]"}' to the topic: 'test/testing'
Published: '{"message": "Hello World [8]"}' to the topic: 'test/testing'
Published: '{"message": "Hello World [9]"}' to the topic: 'test/testing'
Published: '{"message": "Hello World [11]"}' to the topic: 'test/testing'
Published: '{"message": "Hello World [12]"}' to the topic: 'test/testing'
Published: '{"message": "Hello World [13]"}' to the topic: 'test/testing'
Published: '{"message": "Hello World [14]"}' to the topic: 'test/testing'
Published: '{"message": "Hello World [15]"}' to the topic: 'test/testing'
Published: '{"message": "Hello World [16]"}' to the topic: 'test/testing'
Published: '{"message": "Hello World [17]"}' to the topic: 'test/testing'
Published: '{"message": "Hello World [18]"}' to the topic: 'test/testing'
Published: '{"message": "Hello World [19]"}' to the topic: 'test/testing'
Published: '{"message": "Hello World [20]"}' to the topic: 'test/testing'
Publish End

Azureの手順

Azure CLIにエクステンションを追加する

一部エクステンションが必要なコマンドがあるため、azure-iotエクステンションをインストールします。

az extension add --name azure-iot

リソース グループを作成する

IoT部分とは直接関係ありませんが、Azureの場合は必ずリソースグループが必要です。

az group create --name iot-resource-group --location japaneast

IoT Hubを作成する

コマンド一発。お試しなのでSKUはF1(無料)としてます。

az iot hub create --name iot-hub-00001 --resource-group iot-resource-group --partition-count 2 --sku F1

デバイスを作成する

コマンド一発

az iot hub device-identity create --hub-name iot-hub-00001 --device-id Device001

送信側のプログラムを作成する

https://github.com/Azure-Samples/azure-iot-samples-python/blob/master/iot-hub/Quickstarts/simulated-device/SimulatedDevice.py
にあるサンプルを使います。

IoT Hub Device SDKのインストール

pip install azure-iot-device

ソースコードの変更

変数名 内容
CONNECTION_STRING *2で取得できます

*2 az iot hub device-identity connection-string show --hub-name iot-hub-00001 --device-id Device001コマンド

実行結果

無限に続きます

IoT Hub Quickstart #1 - Simulated device
Press Ctrl-C to exit
IoT Hub device sending periodic messages, press Ctrl-C to exit
Sending message: {"temperature": 22.73671571030419,"humidity": 65.13300283503716}
Message successfully sent
Sending message: {"temperature": 21.122891449050375,"humidity": 75.35478976197727}
Message successfully sent
Sending message: {"temperature": 30.11015190710952,"humidity": 79.1313503131281}
Message successfully sent
Sending message: {"temperature": 29.056883680577876,"humidity": 74.9253608733604}
Message successfully sent
Sending message: {"temperature": 30.35374671931261,"humidity": 73.57241118544626}
Message successfully sent
Sending message: {"temperature": 33.336413834339076,"humidity": 65.31133008367256}
Message successfully sent
Sending message: {"temperature": 34.92260215374919,"humidity": 69.53101153342156}
Message successfully sent

確認方法について

AWSとAzureでメッセージが届いたか確認する方法が違ったので、紹介します。

AWSの場合

マネジメントコンソールのテストで確認できます。

image.png

Azureの場合

CLIコマンドで確認できます。

az iot hub monitor-events --hub-name iot-hub-00001 --device-id Device001
Starting event monitor, filtering on device: Device001, use ctrl-c to stop...
{
    "event": {
        "origin": "Device001",
        "module": "",
        "interface": "",
        "component": "",
        "payload": "{\"temperature\": 24.86829506815134,\"humidity\": 62.82101201700818}"
    }
}
{
    "event": {
        "origin": "Device001",
        "module": "",
        "interface": "",
        "component": "",
        "payload": "{\"temperature\": 27.671191300371653,\"humidity\": 70.30860685264159}"
    }
}
{
    "event": {
        "origin": "Device001",
        "module": "",
        "interface": "",
        "component": "",
        "payload": "{\"temperature\": 22.581311567865644,\"humidity\": 66.70979111038993}"
    }
}
Stopping event monitor...

おしまい。

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

AWS日記17 (API Gateway)

はじめに

今回は API Gateway の WebSocket を試します。
簡易なチャットページを作成します。
Lambda関数・SAMテンプレート

準備

AWS SAM CLI環境の準備をします

[Amazon API Gatewayの資料]
Amazon API Gateway
Amazon API Gateway とは
Amazon API Gateway の料金

AWS SAM テンプレート作成

AWS SAM テンプレートで API-Gateway , Lambda , DynamoDb, S3 の設定をします。

[参考資料]
AWS SAM テンプレートを作成する

template.yml
template.yml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Serverless Chat Page

Parameters:
  ApplicationName:
    Type: String
    Default: 'Serverless Chat Page'
  ChatWebSocketApiName:
    Type: String
    Default: 'ChatWebSocket'
  ChatFrontApiName:
    Type: String
    Default: 'ChatFront'
  ChatOnConnectFunctionName:
    Type: String
    Default: 'ChatOnConnectFunction'
  ChatOnDisconnectFunctionName:
    Type: String
    Default: 'ChatOnDisconnectFunction'
  ChatOnSendFunctionName:
    Type: String
    Default: 'ChatOnSendFunction'
  ChatCronFunctionName:
    Type: String
    Default: 'ChatCronFunction'
  ChatFrontFunctionName:
    Type: String
    Default: 'ChatFrontFunction'
  ConnectionTableName:
    Type: String
    Default: 'chat_connection'
  MessageTableName:
    Type: String
    Default: 'chat_message'
  LimitConnectionCount:
    Type: String
    Default: '10'
  LimitMessageCount:
    Type: String
    Default: '100'

Metadata:
  AWS::ServerlessRepo::Application:
    Name: Serverless-Application-Simple-Chat
    Description: 'Serverless Application Simple Chat'
    Author: tanaka-takurou
    SpdxLicenseId: MIT
    LicenseUrl: LICENSE.txt
    ReadmeUrl: README.md
    Labels: ['ServerlessRepo']
    HomePageUrl: https://github.com/tanaka-takurou/serverless-chat-page-go/
    SemanticVersion: 0.0.1
    SourceCodeUrl: https://github.com/tanaka-takurou/serverless-chat-page-go/

Resources:
  ServerlessChatWebSocket:
    Type: AWS::ApiGatewayV2::Api
    Properties:
      Name: !Ref ChatWebSocketApiName
      ProtocolType: WEBSOCKET
      RouteSelectionExpression: "$request.body.action"
  ConnectRoute:
    Type: AWS::ApiGatewayV2::Route
    Properties:
      ApiId: !Ref ServerlessChatWebSocket
      RouteKey: $connect
      AuthorizationType: NONE
      OperationName: ConnectRoute
      Target: !Join
        - '/'
        - - 'integrations'
          - !Ref ConnectInteg
  ConnectInteg:
    Type: AWS::ApiGatewayV2::Integration
    Properties:
      ApiId: !Ref ServerlessChatWebSocket
      Description: Connect Integration
      IntegrationType: AWS_PROXY
      IntegrationUri:
        Fn::Sub:
            arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OnConnectFunction.Arn}/invocations
  DisconnectRoute:
    Type: AWS::ApiGatewayV2::Route
    Properties:
      ApiId: !Ref ServerlessChatWebSocket
      RouteKey: $disconnect
      AuthorizationType: NONE
      OperationName: DisconnectRoute
      Target: !Join
        - '/'
        - - 'integrations'
          - !Ref DisconnectInteg
  DisconnectInteg:
    Type: AWS::ApiGatewayV2::Integration
    Properties:
      ApiId: !Ref ServerlessChatWebSocket
      Description: Disconnect Integration
      IntegrationType: AWS_PROXY
      IntegrationUri:
        Fn::Sub:
            arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OnDisconnectFunction.Arn}/invocations
  SendRoute:
    Type: AWS::ApiGatewayV2::Route
    Properties:
      ApiId: !Ref ServerlessChatWebSocket
      RouteKey: send
      AuthorizationType: NONE
      OperationName: SendRoute
      Target: !Join
        - '/'
        - - 'integrations'
          - !Ref SendInteg
  SendInteg:
    Type: AWS::ApiGatewayV2::Integration
    Properties:
      ApiId: !Ref ServerlessChatWebSocket
      Description: Send Integration
      IntegrationType: AWS_PROXY
      IntegrationUri:
        Fn::Sub:
            arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OnSendFunction.Arn}/invocations
  Deployment:
    Type: AWS::ApiGatewayV2::Deployment
    DependsOn:
    - ConnectRoute
    - SendRoute
    - DisconnectRoute
    Properties:
      ApiId: !Ref ServerlessChatWebSocket
  Stage:
    Type: AWS::ApiGatewayV2::Stage
    Properties:
      StageName: Prod
      Description: Prod Stage
      DeploymentId: !Ref Deployment
      ApiId: !Ref ServerlessChatWebSocket
  ConnectionTable:
    Type: AWS::DynamoDB::Table
    Properties:
      AttributeDefinitions:
      - AttributeName: "connectionId"
        AttributeType: "S"
      KeySchema:
      - AttributeName: "connectionId"
        KeyType: "HASH"
      ProvisionedThroughput:
        ReadCapacityUnits: 5
        WriteCapacityUnits: 5
      SSESpecification:
        SSEEnabled: True
      TableName: !Ref ConnectionTableName
  MessageTable:
    Type: AWS::DynamoDB::Table
    Properties:
      AttributeDefinitions:
      - AttributeName: "id"
        AttributeType: "N"
      KeySchema:
      - AttributeName: "id"
        KeyType: "HASH"
      ProvisionedThroughput:
        ReadCapacityUnits: 5
        WriteCapacityUnits: 5
      SSESpecification:
        SSEEnabled: True
      TableName: !Ref MessageTableName
  ImgBucket:
    Type: AWS::S3::Bucket
  OnConnectFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Ref ChatOnConnectFunctionName
      CodeUri: api/connect/bin/
      Handler: main
      MemorySize: 256
      Runtime: go1.x
      Description: 'Chat OnConnect Function'
      Environment:
        Variables:
          CONNECTION_TABLE_NAME: !Ref ConnectionTableName
          LIMIT_MESSAGE_COUNT: !Ref LimitMessageCount
          LIMIT_CONNECTION_COUNT: !Ref LimitConnectionCount
          REGION: !Ref 'AWS::Region'
      Policies:
      - DynamoDBCrudPolicy:
          TableName: !Ref ConnectionTableName
  OnConnectPermission:
    Type: AWS::Lambda::Permission
    DependsOn:
      - ServerlessChatWebSocket
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref OnConnectFunction
      Principal: apigateway.amazonaws.com
  OnDisconnectFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Ref ChatOnDisconnectFunctionName
      CodeUri: api/disconnect/bin/
      Handler: main
      MemorySize: 256
      Runtime: go1.x
      Description: 'Chat OnDisconnect Function'
      Environment:
        Variables:
          CONNECTION_TABLE_NAME: !Ref ConnectionTableName
          REGION: !Ref 'AWS::Region'
      Policies:
      - DynamoDBCrudPolicy:
          TableName: !Ref ConnectionTableName
  OnDisconnectPermission:
    Type: AWS::Lambda::Permission
    DependsOn:
      - ServerlessChatWebSocket
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref OnDisconnectFunction
      Principal: apigateway.amazonaws.com
  OnSendFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Ref ChatOnSendFunctionName
      CodeUri: api/send/bin/
      Handler: main
      MemorySize: 256
      Runtime: go1.x
      Description: 'Chat OnSendFunction Function'
      Environment:
        Variables:
          CONNECTION_TABLE_NAME: !Ref ConnectionTableName
          MESSAGE_TABLE_NAME: !Ref MessageTableName
          BUCKET_NAME: !Ref ImgBucket
          LIMIT_MESSAGE_COUNT: !Ref LimitMessageCount
          LIMIT_CONNECTION_COUNT: !Ref LimitConnectionCount
          REGION: !Ref 'AWS::Region'
      Policies:
      - DynamoDBCrudPolicy:
          TableName: !Ref ConnectionTableName
      - DynamoDBCrudPolicy:
          TableName: !Ref MessageTableName
      - S3CrudPolicy:
          BucketName: !Ref ImgBucket
      - Statement:
        - Effect: Allow
          Action:
          - 'execute-api:ManageConnections'
          Resource:
          - !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessChatWebSocket}/*'
  SendMessagePermission:
    Type: AWS::Lambda::Permission
    DependsOn:
      - ServerlessChatWebSocket
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref OnSendFunction
      Principal: apigateway.amazonaws.com
  ServerlessChatFrontPage:
    Type: AWS::Serverless::HttpApi
  FrontPageFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Ref ChatFrontFunctionName
      CodeUri: bin/
      Handler: main
      MemorySize: 256
      Runtime: go1.x
      Description: 'Chat Front Function'
      Events:
        testapi:
          Type: HttpApi
          Properties:
            Path: '/'
            Method: get
            ApiId: !Ref ServerlessChatFrontPage
      Environment:
        Variables:
          BUCKET_NAME: !Ref ImgBucket
          MESSAGE_TABLE_NAME: !Ref MessageTableName
          LIMIT_MESSAGE_COUNT: !Ref LimitMessageCount
          WEBSOCKET_URL: !Join [ '', [ 'wss://', !Ref ServerlessChatWebSocket, '.execute-api.',!Ref 'AWS::Region','.amazonaws.com/Prod'] ]
          REGION: !Ref 'AWS::Region'
      Policies:
      - DynamoDBCrudPolicy:
          TableName: !Ref MessageTableName
  ChatApiPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref FrontPageFunction
      Principal: apigateway.amazonaws.com
  CronFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Ref ChatCronFunctionName
      CodeUri: api/cron/bin/
      Handler: main
      MemorySize: 256
      Runtime: go1.x
      Description: 'Chat Cron Function'
      Environment:
        Variables:
          CONNECTION_TABLE_NAME: !Ref ConnectionTableName
          REGION: !Ref 'AWS::Region'
          STACK_NAME: !Ref 'AWS::StackName'
      Policies:
      - DynamoDBCrudPolicy:
          TableName: !Ref ConnectionTableName
  ScheduledRule:
    Type: AWS::Events::Rule
    Properties:
      Description: ScheduledRule
      ScheduleExpression: 'rate(24 hours)'
      State: 'ENABLED'
      Targets:
        - Arn: !GetAtt CronFunction.Arn
          Id: TargetCronFunction
  CronFunctionPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref CronFunction
      Action: lambda:InvokeFunction
      Principal: 'events.amazonaws.com'
      SourceArn: !GetAtt ScheduledRule.Arn

Outputs:
  WebSocketURI:
    Description: "The WSS Protocol URI to connect to"
    Value: !Join [ '', [ 'wss://', !Ref ServerlessChatWebSocket, '.execute-api.',!Ref 'AWS::Region','.amazonaws.com/Prod'] ]

  FrontPageURI:
    Description: "The Front Page URI to connect to"
    Value: !Join [ '', [ 'https://', !Ref ServerlessChatFrontPage, '.execute-api.',!Ref 'AWS::Region','.amazonaws.com/'] ]

WebSocket用のAPI-Gatewayの設定

  ServerlessChatWebSocket:
    Type: AWS::ApiGatewayV2::Api
    Properties:
      Name: !Ref ChatWebSocketApiName
      ProtocolType: WEBSOCKET
      RouteSelectionExpression: "$request.body.action"

WebSocket接続用のルートの設定

  ConnectRoute:
    Type: AWS::ApiGatewayV2::Route
    Properties:
      ApiId: !Ref ServerlessChatWebSocket
      RouteKey: $connect
      AuthorizationType: NONE
      OperationName: ConnectRoute
      Target: !Join
        - '/'
        - - 'integrations'
          - !Ref ConnectInteg
  ConnectInteg:
    Type: AWS::ApiGatewayV2::Integration
    Properties:
      ApiId: !Ref ServerlessChatWebSocket
      Description: Connect Integration
      IntegrationType: AWS_PROXY
      IntegrationUri:
        Fn::Sub:
            arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OnConnectFunction.Arn}/invocations

WebSocket接続先にデータを送る用のルートの設定

  SendRoute:
    Type: AWS::ApiGatewayV2::Route
    Properties:
      ApiId: !Ref ServerlessChatWebSocket
      RouteKey: send
      AuthorizationType: NONE
      OperationName: SendRoute
      Target: !Join
        - '/'
        - - 'integrations'
          - !Ref SendInteg
  SendInteg:
    Type: AWS::ApiGatewayV2::Integration
    Properties:
      ApiId: !Ref ServerlessChatWebSocket
      Description: Send Integration
      IntegrationType: AWS_PROXY
      IntegrationUri:
        Fn::Sub:
            arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OnSendFunction.Arn}/invocations

WebSocket切断用のルートの設定

  DisconnectRoute:
    Type: AWS::ApiGatewayV2::Route
    Properties:
      ApiId: !Ref ServerlessChatWebSocket
      RouteKey: $disconnect
      AuthorizationType: NONE
      OperationName: DisconnectRoute
      Target: !Join
        - '/'
        - - 'integrations'
          - !Ref DisconnectInteg
  DisconnectInteg:
    Type: AWS::ApiGatewayV2::Integration
    Properties:
      ApiId: !Ref ServerlessChatWebSocket
      Description: Disconnect Integration
      IntegrationType: AWS_PROXY
      IntegrationUri:
        Fn::Sub:
            arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OnDisconnectFunction.Arn}/invocations

Lambda関数作成

※ Lambda関数は aws-lambda-go を利用し、apigatewayの周りの処理は aws-sdk-go-v2 を利用しました。

WebsocketのコネクションIDを取得するには APIGatewayWebsocketProxyRequest.RequestContext.ConnectionID を使う

func HandleRequest(ctx context.Context, request events.APIGatewayWebsocketProxyRequest) (Response, error) {

        ...

    if err == nil && int(*connectionCount) < limitCount {
        err = putConnection(ctx, request.RequestContext.ConnectionID)
    } else if int(*connectionCount) >= limitCount {
        err = errors.New("too many connections")
    }

        ...

}

WebSocket接続先にデータを送るには PostToConnectionRequest を使う

connectionRequest := apigatewayClient.PostToConnectionRequest(&apigatewaymanagementapi.PostToConnectionInput{
    Data:         jsonBytes,
    ConnectionId: &connectionId,
})
_, err := connectionRequest.Send(ctx)

終わりに

これまでAPI Gatewayは、ほぼREST APIのみ利用してきましたが、用途に合わせて HTTP API や WebSocket API も使い分けていこうと思います。

参考資料

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

AWS日記16 (Serverless Application Repository)

はじめに

今回は AWS Serverless Application Repository を試します。
サーバーレスアプリケーションを管理するページを作成します。
Lambda関数・SAMテンプレート

準備

サーバーレスアプリケーションの準備をします

Serverless Application Repositoryの「マイアプリケーション」にアプリケーションが表示されている状態にします。
serverlessrepo.jpg

[AWS Serverless Application Repositoryの資料]
AWS Serverless Application Repository
よくある質問と規約
リポジトリへのアプリケーションの公開

AWS SAM テンプレート作成

AWS SAM テンプレートで API-Gateway , Lambdaの設定をします。

[参考資料]
AWS SAM テンプレートを作成する

template.yml
template.yml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Serverless Application Management Page

Parameters:
  ApplicationName:
    Type: String
    Default: 'ServerlessApplicationManagementPage'
  FrontPageApiStageName:
    Type: String
    Default: 'ProdStage'

Resources:
  FrontPageApi:
    Type: AWS::Serverless::Api
    Properties:
      Name: ServerlessApplicationManagementPageApi
      EndpointConfiguration: REGIONAL
      StageName: !Ref FrontPageApiStageName
  FrontPageFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: ServerlessApplicationManagementPageFrontFunction
      CodeUri: bin/
      Handler: main
      MemorySize: 256
      Runtime: go1.x
      Description: 'ApplicationManagement Function'
      Policies:
      Environment:
        Variables:
          REGION: !Ref 'AWS::Region'
          API_PATH: !Join [ '', [ '/', !Ref FrontPageApiStageName, '/api'] ]
      Events:
        FrontPageApi:
          Type: Api
          Properties:
            Path: '/'
            Method: get
            RestApiId: !Ref FrontPageApi
  MainFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: ServerlessApplicationManagementPageApiFunction
      CodeUri: api/bin/
      Handler: main
      MemorySize: 256
      Runtime: go1.x
      Description: 'ApplicationManagement Function'
      Role: !GetAtt MainFunctionRole.Arn
      Environment:
        Variables:
          REGION: !Ref 'AWS::Region'
      Events:
        FrontPageApi:
          Type: Api
          Properties:
            Path: '/api'
            Method: post
            RestApiId: !Ref FrontPageApi
  MainFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      MaxSessionDuration: 3600
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: 'Allow'
            Principal:
              Service:
                - 'lambda.amazonaws.com'
            Action:
              - 'sts:AssumeRole'
      Policies:
        - PolicyName: ManagementApplicationPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: 'Allow'
                Action:
                  - 'serverlessrepo:ListApplications'
                  - 'serverlessrepo:CreateCloudFormationTemplate'
                Resource: '*'
              - Effect: 'Allow'
                Action:
                  - 'logs:CreateLogGroup'
                  - 'logs:CreateLogStream'
                  - 'logs:PutLogEvents'
                Resource: '*'
              - Effect: 'Allow'
                Action:
                  - 'cloudformation:DescribeStackResources'
                  - 'cloudformation:DeleteStack'
                  - 'cloudformation:CreateStack'
                  - 'cloudformation:ListStacks'
                  - 'cloudformation:ListStackResources'
                  - 'cloudformation:CreateChangeSet'
                Resource: '*'
              - Effect: 'Allow'
                Action:
                  - 'lambda:*'
                  - 'events:RemoveTargets'
                  - 'events:PutTargets'
                  - 'events:DescribeRule'
                  - 'events:DeleteRule'
                  - 'events:PutRule'
                  - 'iam:DeleteRolePolicy'
                  - 'iam:DeleteRole'
                  - 'iam:CreateRole'
                  - 'iam:AttachRolePolicy'
                  - 'iam:PutRolePolicy'
                  - 'iam:GetRole'
                  - 'iam:PassRole'
                Resource: '*'
              - Effect: 'Allow'
                Action:
                  - 's3:PutObject'
                  - 's3:GetObject'
                Resource: '*'
              - Effect: 'Allow'
                Action:
                  - 'apigateway:*'
                Resource: '*'

Outputs:
  APIURI:
    Description: "URI"
    Value: !Join [ '', [ 'https://', !Ref FrontPageApi, '.execute-api.',!Ref 'AWS::Region','.amazonaws.com/',!Ref FrontPageApiStageName,'/'] ]

API-Gatewayの設定

  FrontPageApi:
    Type: AWS::Serverless::Api
    Properties:
      Name: ServerlessApplicationManagementPageApi
      EndpointConfiguration: REGIONAL
      StageName: !Ref FrontPageApiStageName

フロントエンド用Lambdaの設定

  FrontPageFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: ServerlessApplicationManagementPageFrontFunction
      CodeUri: bin/
      Handler: main
      MemorySize: 256
      Runtime: go1.x
      Description: 'ApplicationManagement Function'
      Policies:
      Environment:
        Variables:
          REGION: !Ref 'AWS::Region'
          API_PATH: !Join [ '', [ '/', !Ref FrontPageApiStageName, '/api'] ]
      Events:
        FrontPageApi:
          Type: Api
          Properties:
            Path: '/'
            Method: get
            RestApiId: !Ref FrontPageApi

バックエンド用Lambdaの設定

  MainFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: ServerlessApplicationManagementPageApiFunction
      CodeUri: api/bin/
      Handler: main
      MemorySize: 256
      Runtime: go1.x
      Description: 'ApplicationManagement Function'
      Role: !GetAtt MainFunctionRole.Arn
      Environment:
        Variables:
          REGION: !Ref 'AWS::Region'
      Events:
        FrontPageApi:
          Type: Api
          Properties:
            Path: '/api'
            Method: post
            RestApiId: !Ref FrontPageApi

Lambda関数作成

main.go
main.go
package main

import (
    "os"
    "fmt"
    "log"
    "time"
    "context"
    "strings"
    "net/http"
    "encoding/json"
    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/aws/external"
    "github.com/aws/aws-sdk-go-v2/service/cloudformation"
    "github.com/aws/aws-sdk-go-v2/service/serverlessapplicationrepository"
)

type Application struct {
    Name        string `json:"name"`
    Description string `json:"description"`
    Stack       Stack  `json:"stack"`
}

type Stack struct {
    Name   string `json:"name"`
    Status string `json:"status"`
    Url    string `json:"url"`
}

type APIResponse struct {
    Message         string        `json:"message"`
    ApplicationList []Application `json:"applicationList"`
}

type Response events.APIGatewayProxyResponse

var cfg aws.Config
var cloudformationClient *cloudformation.Client
var serverlessApplicationRepositoryClient *serverlessapplicationrepository.Client

const layout string = "20060102150405.000"

func HandleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (Response, error) {
    var jsonBytes []byte
    var err error
    d := make(map[string]string)
    json.Unmarshal([]byte(request.Body), &d)
    if v, ok := d["action"]; ok {
        switch v {
        case "status" :
            l, e := getApplications(ctx)
            if e != nil {
                err = e
            } else {
                jsonBytes, _ = json.Marshal(APIResponse{Message: "Success.", ApplicationList: l})
            }
        case "create" :
            if n, ok := d["name"]; ok {
                e := createStack(ctx, n)
                if e != nil {
                    err = e
                } else {
                    jsonBytes, _ = json.Marshal(APIResponse{Message: "Success.", ApplicationList: nil})
                }
            }
        case "delete" :
            if n, ok := d["name"]; ok {
                e := deleteStack(ctx, n)
                if e != nil {
                    err = e
                } else {
                    jsonBytes, _ = json.Marshal(APIResponse{Message: "Success.", ApplicationList: nil})
                }
            }
        }
    }
    log.Print(request.RequestContext.Identity.SourceIP)
    if err != nil {
        log.Print(err)
        jsonBytes, _ = json.Marshal(APIResponse{Message: fmt.Sprint(err)})
        return Response{
            StatusCode: http.StatusInternalServerError,
            Body: string(jsonBytes),
        }, nil
    }
    return Response {
        StatusCode: http.StatusOK,
        Body: string(jsonBytes),
    }, nil
}

func getApplications(ctx context.Context)([]Application, error) {
    if serverlessApplicationRepositoryClient == nil {
        serverlessApplicationRepositoryClient = serverlessapplicationrepository.New(cfg)
    }
    req := serverlessApplicationRepositoryClient.ListApplicationsRequest(&serverlessapplicationrepository.ListApplicationsInput{})
    res, err := req.Send(ctx)
    if err != nil {
        return nil, err
    }
    var applicationList []Application
    for _, i := range res.ListApplicationsOutput.Applications {
        applicationList = append(applicationList, Application{
            Name:        aws.StringValue(i.Name),
            Description: aws.StringValue(i.Description),
            Stack:       Stack{},
        })
    }
    applicationList, err = addStackData(ctx, applicationList)
    if err != nil {
        return nil, err
    }
    return applicationList, nil
}

func getApplicationId(ctx context.Context, name string)(string, error) {
    if serverlessApplicationRepositoryClient == nil {
        serverlessApplicationRepositoryClient = serverlessapplicationrepository.New(cfg)
    }
    req := serverlessApplicationRepositoryClient.ListApplicationsRequest(&serverlessapplicationrepository.ListApplicationsInput{})
    res, err := req.Send(ctx)
    if err != nil {
        return "", err
    }
    var applicationId string
    for _, i := range res.ListApplicationsOutput.Applications {
        if name == aws.StringValue(i.Name) {
            applicationId = aws.StringValue(i.ApplicationId)
            break
        }
    }
    return applicationId, nil
}

func getTemplateUrl(ctx context.Context, applicationId string)(string, error) {
    if serverlessApplicationRepositoryClient == nil {
        serverlessApplicationRepositoryClient = serverlessapplicationrepository.New(cfg)
    }
    req := serverlessApplicationRepositoryClient.CreateCloudFormationTemplateRequest(&serverlessapplicationrepository.CreateCloudFormationTemplateInput{
        ApplicationId: aws.String(applicationId),
    })
    res, err := req.Send(ctx)
    if err != nil {
        return "", err
    }
    return aws.StringValue(res.CreateCloudFormationTemplateOutput.TemplateUrl), nil
}

func addStackData(ctx context.Context, applicationList []Application)([]Application, error) {
    if cloudformationClient == nil {
        cloudformationClient = cloudformation.New(cfg)
    }
    req := cloudformationClient.ListStacksRequest(&cloudformation.ListStacksInput{
        StackStatusFilter: []cloudformation.StackStatus{
            cloudformation.StackStatusCreateComplete,
            cloudformation.StackStatusCreateInProgress,
            cloudformation.StackStatusDeleteInProgress,
        },
    })
    res, err := req.Send(ctx)
    if err != nil {
        return nil, err
    }
    for _, i := range res.ListStacksOutput.StackSummaries {
        stackName := aws.StringValue(i.StackName)
        for n, j := range applicationList {
            if strings.HasPrefix(stackName, j.Name) {
                var url string
                if i.StackStatus == cloudformation.StackStatusCreateComplete {
                    req_ := cloudformationClient.ListStackResourcesRequest(&cloudformation.ListStackResourcesInput{StackName: i.StackName})
                    res_, err := req_.Send(ctx)
                    if err != nil {
                        log.Println(err)
                        break
                    }
                    for _, j := range res_.ListStackResourcesOutput.StackResourceSummaries {
                        if aws.StringValue(j.ResourceType) == "AWS::ApiGatewayV2::Api" {
                            url = "https://" + aws.StringValue(j.PhysicalResourceId) + ".execute-api." + os.Getenv("REGION") + ".amazonaws.com/"
                        }
                    }
                }
                applicationList[n].Stack = Stack{Name: stackName, Status: string(i.StackStatus), Url: url}
                break
            }
        }
    }
    return applicationList, nil
}

func createStack(ctx context.Context, name string) error {
    applicationId, err := getApplicationId(ctx, name)
    if err != nil {
        log.Println(err)
        return err
    }
    templateUrl, err := getTemplateUrl(ctx, applicationId)
    if err != nil {
        log.Println(err)
        return err
    }
    t := time.Now()
    stackName := name + strings.Replace(t.Format(layout), ".", "", 1)
    if cloudformationClient == nil {
        cloudformationClient = cloudformation.New(cfg)
    }
    req := cloudformationClient.CreateStackRequest(&cloudformation.CreateStackInput{
        Capabilities: []cloudformation.Capability{
            cloudformation.CapabilityCapabilityIam,
            cloudformation.CapabilityCapabilityAutoExpand,
        },
        StackName: aws.String(stackName),
        TemplateURL: aws.String(templateUrl),
    })
    _, err = req.Send(ctx)
    if err != nil {
        log.Println(err)
        return err
    }
    return nil
}

func deleteStack(ctx context.Context, name string) error {
    if cloudformationClient == nil {
        cloudformationClient = cloudformation.New(cfg)
    }
    req := cloudformationClient.DeleteStackRequest(&cloudformation.DeleteStackInput{
        StackName: aws.String(name),
    })
    _, err := req.Send(ctx)
    if err != nil {
        log.Println(err)
    }
    return nil
}

func getTargetStack(name string, list []Stack) Stack {
    var stack Stack
    for _, i := range list {
        if i.Name == name {
            stack = i
            break
        }
    }
    return stack
}

func init() {
    var err error
    cfg, err = external.LoadDefaultAWSConfig()
    cfg.Region = os.Getenv("REGION")
    if err != nil {
        log.Print(err)
    }
}

func main() {
    lambda.Start(HandleRequest)
}

アプリケーション一覧を取得するには ListApplicationsRequest を使う

req := serverlessApplicationRepositoryClient.ListApplicationsRequest(&serverlessapplicationrepository.ListApplicationsInput{})
res, err := req.Send(ctx)

※ Serverless Application Repositoryのマイアプリケーションに表示されているアプリケーション一覧が取得できます。

CloudFormationテンプレートを作成するには CreateCloudFormationTemplateRequest を使う

req := serverlessApplicationRepositoryClient.CreateCloudFormationTemplateRequest(&serverlessapplicationrepository.CreateCloudFormationTemplateInput{
    ApplicationId: aws.String(applicationId),
})
res, err := req.Send(ctx)

スタックを作成するには CreateStackRequest を使う

req := cloudformationClient.CreateStackRequest(&cloudformation.CreateStackInput{
    Capabilities: []cloudformation.Capability{
        cloudformation.CapabilityCapabilityIam,
        cloudformation.CapabilityCapabilityAutoExpand,
    },
    StackName: aws.String(stackName),
    TemplateURL: aws.String(templateUrl),
})
_, err = req.Send(ctx)

スタック一覧を取得するには ListStacksRequest を使う

req := cloudformationClient.ListStacksRequest(&cloudformation.ListStacksInput{
    StackStatusFilter: []cloudformation.StackStatus{
        cloudformation.StackStatusCreateComplete,
        cloudformation.StackStatusCreateInProgress,
        cloudformation.StackStatusDeleteInProgress,
    },
})
res, err := req.Send(ctx)

上記の例では、フィルターを設定し [作成完了、作成中、削除中]のスタックのみを取得しています。

スタックを削除するには DeleteStackRequest を使う

req := cloudformationClient.DeleteStackRequest(&cloudformation.DeleteStackInput{
    StackName: aws.String(name),
})
_, err := req.Send(ctx)

終わりに

Serverless Application Repositoryにマイアプリケーションを発行することで、Web環境のみの状況でもサーバレスアプリケーションを作成することができます。
AWS-SAMとあわせて使い慣れていこうと思います。

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

AWS日記15 (Serverless Application Model)

はじめに

今回は AWS Serverless Application Model (AWS SAM)を試します。
アクセスすると自己削除するWebページを作成します。
Lambda関数・SAMテンプレート

準備

AWS SAM CLI をインストールします
S3の準備をします

[AWS SAMの資料]
AWS サーバーレスアプリケーションモデル
AWS Serverless Application Model

AWS SAM テンプレート作成

AWS SAM テンプレートで API-Gateway , Lambdaの設定をします。

[参考資料]
AWS SAM テンプレートを作成する

template.yml
template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Serverless Application Ephemerality

Parameters:
  ApplicationName:
    Type: String
    Default: 'ServerlessApplicationEphemeralityMinimum'
  EphemeralityFunctionName:
    Type: String
    Default: 'EphemeralityFunctionMinimum'

Metadata:
  AWS::ServerlessRepo::Application:
    Name: Serverless-Application-Minimum
    Description: 'This application is deleted when accessed.'
    Author: tanaka-takurou
    SpdxLicenseId: MIT
    LicenseUrl: LICENSE.txt
    ReadmeUrl: README.md
    Labels: ['ServerlessRepo']
    HomePageUrl: https://github.com/tanaka-takurou/serverless-application-ephemerality-page-go/tree/minimum
    SemanticVersion: 0.0.2
    SourceCodeUrl: https://github.com/tanaka-takurou/serverless-application-ephemerality-page-go/tree/minimum

Resources:
  EphemeralityApi:
    Type: AWS::Serverless::HttpApi
  EphemeralityFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Ref EphemeralityFunctionName
      CodeUri: bin/
      Handler: main
      MemorySize: 256
      Runtime: go1.x
      Description: 'Front Function'
      Role: !GetAtt EphemeralityFunctionRole.Arn
      Events:
        EphemeralityApi:
          Type: HttpApi
          Properties:
            Path: '/'
            Method: get
            ApiId: !Ref EphemeralityApi
      Environment:
        Variables:
          COUNT: "0"
          LIMIT: "0"
          REGION: !Ref AWS::Region
          STACK_NAME: !Ref AWS::StackName
          FUNCTION_NAME: !Ref EphemeralityFunctionName
  EphemeralityApiPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref EphemeralityFunction
      Principal: apigateway.amazonaws.com
  EphemeralityFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      MaxSessionDuration: 3600
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: 'Allow'
            Principal:
              Service:
                - 'lambda.amazonaws.com'
            Action:
              - 'sts:AssumeRole'
      Policies:
        - PolicyName: KillFunctionPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: 'Allow'
                Action:
                  - 'logs:CreateLogGroup'
                  - 'logs:CreateLogStream'
                  - 'logs:PutLogEvents'
                Resource: '*'
              - Effect: 'Allow'
                Action:
                  - 'cloudformation:DescribeStackResources'
                  - 'cloudformation:DeleteStack'
                Resource: '*'
              - Effect: 'Allow'
                Action:
                  - 'lambda:*'
                  - 'events:RemoveTargets'
                  - 'events:DeleteRule'
                  - 'iam:DeleteRolePolicy'
                  - 'iam:DeleteRole'
                Resource: '*'
              - Effect: 'Allow'
                Action:
                  - 'apigateway:*'
                Resource: '*'

Outputs:
  APIURI:
    Value: !Join [ '', [ 'https://', !Ref EphemeralityApi, '.execute-api.',!Ref 'AWS::Region','.amazonaws.com/'] ]

API-Gatewayの設定は以下の部分

  EphemeralityApi:
    Type: AWS::Serverless::HttpApi

Lambdaの設定は以下の部分

  EphemeralityFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Ref EphemeralityFunctionName
      CodeUri: bin/
      Handler: main
      MemorySize: 256
      Runtime: go1.x
      Description: 'Front Function'
      Role: !GetAtt EphemeralityFunctionRole.Arn
      Events:
        EphemeralityApi:
          Type: HttpApi
          Properties:
            Path: '/'
            Method: get
            ApiId: !Ref EphemeralityApi
      Environment:
        Variables:
          COUNT: "0"
          LIMIT: "0"
          REGION: !Ref AWS::Region
          STACK_NAME: !Ref AWS::StackName
          FUNCTION_NAME: !Ref EphemeralityFunctionName

Lambda関数作成

main.go
main.go
package main

import (
    "os"
    "log"
    "context"
    "strconv"
    "net/http"
    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/aws/external"
    slambda "github.com/aws/aws-sdk-go-v2/service/lambda"
    "github.com/aws/aws-sdk-go-v2/service/cloudformation"
)

var cfg aws.Config

func HandleRequest(ctx context.Context, request events.APIGatewayV2HTTPRequest) (events.APIGatewayProxyResponse, error) {
    count, _ := strconv.Atoi(os.Getenv("COUNT"))
    limit, _ := strconv.Atoi(os.Getenv("LIMIT"))
    if count < limit {
        client := slambda.New(cfg)
        req := client.GetFunctionConfigurationRequest(&slambda.GetFunctionConfigurationInput{
            FunctionName: aws.String(os.Getenv("FUNCTION_NAME")),
        })
        res, err := req.Send(ctx)
        if err != nil {
            log.Println(err)
        } else {
            env := res.GetFunctionConfigurationOutput.Environment.Variables
            env["COUNT"] = strconv.Itoa(count + 1)
            req_ := client.UpdateFunctionConfigurationRequest(&slambda.UpdateFunctionConfigurationInput{
                FunctionName: aws.String(os.Getenv("FUNCTION_NAME")),
                Environment: &slambda.Environment{
                    Variables: env,
                },
            })
            _, err := req_.Send(ctx)
            if err != nil {
                log.Println(err)
            }
        }
    } else {
        client := cloudformation.New(cfg)
        req := client.DeleteStackRequest(&cloudformation.DeleteStackInput{
            StackName: aws.String(os.Getenv("STACK_NAME")),
        })
        _, err := req.Send(ctx)
        if err != nil {
            log.Println(err)
        }
    }
    return events.APIGatewayProxyResponse{
        StatusCode:      http.StatusOK,
        IsBase64Encoded: false,
        Body:            "<html><head><title>Serverless Application Ephemerality</title></head><body><span>Serverless Application Ephemerality</span></body></html>",
        Headers: map[string]string{
            "Content-Type": "text/html",
        },
    }, nil
}

func init() {
    var err error
    cfg, err = external.LoadDefaultAWSConfig()
    cfg.Region = os.Getenv("REGION")
    if err != nil {
        log.Print(err)
    }
}

func main() {
    lambda.Start(HandleRequest)
}

スタックを削除するには DeleteStackRequest を使う

client := cloudformation.New(cfg)
req := client.DeleteStackRequest(&cloudformation.DeleteStackInput{
    StackName: aws.String(os.Getenv("STACK_NAME")),
})
_, err := req.Send(ctx)

※ この処理により 作成したWebページが削除されます。

デプロイ

sam package --output-template-file packaged.yml --s3-bucket "${bucket}"
sam deploy --stack-name "${stack}" --capabilities CAPABILITY_IAM --template-file packaged.yml

※ ${bucket} には 「準備」 で作成した S3バケット名を入力
※ ${stack} には スタック名を入力 (既にあるスタックを指定すると、スタックを更新します)

・デプロイが完了すると、作成したWebページにアクセスできるようになります。

・デプロイ完了後 CloudFormationのスタック一覧 に表示されます。

パブリッシュ

sam package --output-template-file packaged.yml --s3-bucket "${bucket}"
sam publish --template packaged.yml" --region "${AWS_DEFAULT_REGION}"

※ ${bucket} には 「準備」 で作成した S3バケット名を入力
※ ${AWS_DEFAULT_REGION} には リージョン (ap-northeast-1 など) を入力

・パブリッシュ完了後 Serverless Application Repositoryのマイアプリケーション一覧 に表示されます。

削除

aws cloudformation delete-stack --stack-name "${stack}"

※ ${stack} には スタック名を入力

終わりに

Policy周りが原因でデプロイする際にエラーが発生することが多くありました。
使い慣れることで、AWS管理画面よりも簡単に、サーバレスアプリケーションの作成・削除・複製できそうです。

参考資料

serverless-application-model
究極のCloudFormationをたずねて三千里
AWS SAMを使ってみる
AWS SAMのコマンドをまとめてみた
AWS SAM アプリケーションをデプロイする
今日から始めるサーバーレス SAM【API Gateway + Lambda + DynamoDB】
SAMで作成されるApiGatewayをエッジ最適化以外で作成したいとき
aws-sam-cliでLambda,DynamoDBのサーバーレスアプリケーション開発に入門してみる
AWS SAM で Hello World する
ゼロから始める AWS SAM 入門
SAM で API Gateway の CloudWatch Logs を有効にしたい
CloudFormationでクソが!って叫んだこと
SAMでtemplate.yamlの記述方法(Events、Policyはここを参照して書く)
AWS SAM CLI 再入門 2020.07
AWS SAMでローカル環境でS3とDynamoDBを扱うLambdaを実行する
cloudformation/samでsns通知をslackに流すgoのlambdaを作る
CloudFormation で Cognito
AWS SAMのテンプレートではリソースごとに!Refと!GetAttの戻り値が違う
CloudFormationスタック作成エラー: Requires capabilities : [CAPABILITY_NAMED_IAM]
AWS SAMを利用してGolangなLambdaをデプロイする
多分わかりやすいCloudFormation入門とチュートリアル
CloudFormationでAWS Lambdaを作成・更新する際のベストプラクティス
CloudFormationでAPI Gateway+LambdaなAPIを作成する
AWS SAMを使う前にCloudFormationテンプレートを書こう

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

RDSはデフォルトパラメータグループはやめよう

RDSのMySQLをデフォルトパラメータグループで運用していたのですが、カスタムパラメータグループにしておけばよかったと後悔した話です。

RDSのInnoDBモニターを有効化したかった。

DeadLockなどのログが見たくてInnoDBモニターを有効化しようと思いました。
有効化にはMySQLのDBパラメータinnodb_status_output/innodb_status_output_locksをONにする必要があります。
ですが、デフォルトパラメータグループのパラメータは変えることが出来ません。
そのためカスタムパラメータグループを作成し付け替えたのですが、パラメータグループの付け替えには必ずインスタンスを再起動する必要がありました。

ということでRDSは作成時からカスタムパラメータグループで運用するよう心掛けましょう。

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

#1 Djangoのwebアプリケーションをデプロイするまで(AWSのEC2でインスタンス構築編)

はじめに

私はプログラミング言語の初学者です。
その為、読み間違いや理解不足の部分があると思います。
その場合はコメントで教えて頂けると幸いです。

「ゴール」は事前に作成したPythonのwebアプリケーションをデプロイする所まで行きたいと思います。出来るだけ初学者向けに書くつもりです。

作成手順

  • AWSのEC2でインスタンスを構築後に、SSH通信をする ←現在はここです。
  • 構築したインスタンスの中にPythonの環境を構築する
  • インスタンスの中に仮想環境を構築し、Django等をインストール(フレームワーク)
  • PostgreSQLを設定する(データベース)
  • インターネット上にアプリケーションが接続出来るように設定する(デプロイ前半)
  • NginxとGunicornの設定する(デプロイ後半)

インスタンスの作成の仕方

ステップ1(インスタンスの作成)

AWSのEC2を選択し、この画面に移動します。
画面右上の矢印がある所をクリックします。(ここはリージョンというものを選択する所です。)
*特にこだわりが無ければどこでもいいですが、EC2を使用する前はここを必ずチェックしましょう。
「インスタンスの作成」をクリックします。
インスタンス作成画面.png

ステップ2(Amazon マシンイメージの選択)

「インスタンスを作成」をクリックすると、この画面に移動すると思います。
ここで仮想サーバを決めます。
今回はUbuntu Server 18.04 LTS (HVM), SSD Volume Type - ami-0bcc094591f354be2 (64 ビット x86) / ami-0bc556e0c71e1b467 (64 ビット Arm)を使います。
ステップ1マシンイメージ.png

ステップ3(インスタンスタイプを選択)

インスタンスタイプは「 t2.micro 無料利用枠の対象 」を選択
*ここで「無料利用枠の対象」の物を選択しないと、お金が掛かります。
ステップ2 インスタンスタイプの選択.png

ステップ4(ストレージの設定)

「ステップ7:インスタンス作成の確認」の所まで何も考えずに「次へ」ボタンをクリックします。
*必要なことがあれば、作成した後でも変更ができると思います。
ステップ3 インスタンスの詳細の設定.png
ステップ4 ストレージの追加.png
ステップ5 タグの追加.png
ステップ6 セキュリティグループの設定.png
ステップ7 インスタンス作成の確認.png

ステップ5(キーペアの作成+ダウンロード)

「ステップ7:インスタンス作成の確認」のページの「起動」をクリックします。
すると、この画面になると思いますので「新しいキーペアの作成」を選択します。
「キーペア名」はご自身の好きな名前にしてください。
*ここでは[test]にしておきます。
そしてダウンロードします。
*このインスタンスの使用中は必ず削除しないでください。また誰にも渡らないようにしてください。
ちなみにダウンロードがされると、filderの中にダウンロードがされると思いますのでDesktopに移動しておいてください。後で使います。
キーペアの作成.png

ステップ6(SSH通信をしてみる)

キーペアのダウンロードされているか確認してください。
その後にDesktopの中に移動してあることが前提で進めます。
キーペアはXXX.pemというファイルでダウンロードされていると思いますが、このファイルをchmodコマンドでパーミッションを変更しないとSSH接続できません。

  • 'hostname'ですが、インスタンスの説明の中にあるパブリック DNS: xxx.amazonaws.comです。
  • XXX.pemの'XXX'はステップ5で決めた「キーペア名」になります。
  • ubuntuはデフォルトの名前になります。
パーミッションは下記のコードを打ちます。
$ chmod 400 XXX.pem

SSH接続できるか確認します。
$ ssh -i XXX.pem 'user name'@'hostname'

この記事ではこうなります。
$ ssh -i test.pem ubuntu@ec2.00-000-000-00.compute-1.amazonaws.com

ターミナルで(yes/no)聞かれるので、yesと打ちます。
そうすると
ubuntu@ip-000-00-00-000:~$ というプロンプトになるはずです。

続きは次の記事に書きます。

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