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

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

1.TL;DR

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

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

pre_aws.png

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

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

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

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

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

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

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

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

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

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

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

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

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

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

aws_kansatsu.png

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

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

関連するAWSサービス

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

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

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

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

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

関連するAWSサービス

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

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

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

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

3-3.運用導線の確認

関連するAWSサービス

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

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

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

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

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

server_chosa.png

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

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

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

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

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

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

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

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

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

5.まとめ

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

続きを読む

CloudFormationを使って一撃で作るAssumeRoleによる強い💪権限管理

インフラエンジニアのみつの(@kotatsu360)です。
この記事はVASILY Advent Calendar 201710日目の記事です。

この記事では、AWSのIAMでAssumeRoleする権限周りを一撃でつくるCloudFormationテンプレートをまとめます。ご査収ください。

CloudFormationが作る環境のイメージ
image.png

このドキュメントの目的

IAMは調べれば調べるほど情報が断片的になる。あとJSON書くの面倒(一一 )
CloudFormationならYAMLで書けるし再現性がある。テンプレートにまとめようヽ(゚∀゚)ノ パッ☆

AssumeRoleとは

  1. 役割ごとに必要な権限を持ったIAMロールを作っておく

    • 開発
    • 運用
    • 請求
    • 監査
    • etc…
  2. IAMロールごとに「自分の権限を使うことを許可する」対象を設定しておく
    • 開発用のロールはエンジニアAとBとCと…
    • IAMロール「力がほしい時はいつでも我が名を呼ぶが良い」
  3. つかう
    • 変身
    • 普段はブラックRX。たまにはバイオライダー。

メリット

  • 権限をロールという単位で整理できる

    • 個人ごとに細かく設定するとスパゲッティになりがち
  • 場面場面で必要な権限だけが使える
    • 「開発時に誤って本番構成を変更してしまって泣く」ということが減る
  • 権限の確認が楽
    • 個人ごとに設定すると、権限付与者(おそらく強い権限持ち)と非付与者の差異が生まれる
    • AssumeRoleであれば、そのロールにスイッチして自分で権限を確認できる

デメリット

  • めんどう

    • 「rootアカウントでいいじゃん」と言われたらそこまで・・・
    • アカウント管理をやりたい時はとてもオススメ

なお、後から入れるのはカロリーを消費するので導入は早い方が良いと思います。
また、CloudFormationを使うとコード管理できるので、かなり楽になります。

CloudFormationテンプレートで実行されること

  • IAMユーザの作成

    • ユーザは4人
    • 開発者
    • 開発者&本番運用者
    • 開発者&本番運用者
    • 請求管理者
    • 初ログイン時にパスワード変更画面を出す
  • IAMグループの作成
    • 全員用のグループ
    • 本番運用者用のグループ
  • IAMロールの作成
    • 開発者用の権限を管理するロール
    • 本番運用者用の権限を管理するロール
    • 請求管理者用の権限を管理するロール
  • ユーザをグループに追加

グループとロールの住み分けについては後で触れます。

CloudFormationテンプレート

初期パスワードが小文字と記号になっているので、アカウントのパスワードポリシーによっては作成に失敗します。

AWSTemplateFormatVersion: 2010-09-09
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: 'IAM User Name'
        Parameters:
          - Dev1
          - Prod1
          - Prod2
          - Billing1
Parameters:
  Dev1:
    Type: String
    Default: 'kotatsu360a'
  Prod1:
    Type: String
    Default: 'kotatsu360b'
  Prod2:
    Type: String
    Default: 'kotatsu360c'
  Billing1:
    Type: String
    Default: 'kotatsu360d'
Resources:
  # ユーザ全員が所属するグループ
  IAMGroupAllMember:
    Type: 'AWS::IAM::Group'
    Properties:
      Policies:
        - PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: 'Allow'
                Action:
                  - 'iam:List*'
                Resource: '*'
              - Effect: 'Allow'
                Action:
                  - 'iam:GetLoginProfile'
                  - 'iam:ChangePassword'
                  - 'iam:CreateVirtualMFADevice'
                  - 'iam:DeleteVirtualMFADevice'
                  - 'iam:EnableMFADevice'
                Resource: 
                  - !Sub 'arn:aws:iam::${AWS::AccountId}:mfa/${!aws:username}'
                  - !Sub 'arn:aws:iam::${AWS::AccountId}:user/${!aws:username}'
             - Effect: 'Allow'
                Action:
                  - 'am:DeactivateMFADevice'
                Resource:
                  - !Sub 'arn:aws:iam::${AWS::AccountId}:mfa/${!aws:username}'
                  - !Sub 'arn:aws:iam::${AWS::AccountId}:user/${!aws:username}'
                Condition:
                  Bool:
                    aws:MultiFactorAuthPresent: true
          PolicyName: 'mfa-attach'

  # 本番運用者が所属するグループ
  IAMGroupProductionOperator:
    Type: 'AWS::IAM::Group'
    Properties:
      Policies:
        - PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: 'Allow'
                Action:
                  - 'opsworks:UpdateMyUserProfile'
                  - 'opsworks:Describe*'
                Resource: '*'
          PolicyName: 'change-my-ssh-key'

  # [NOTE] 開発者と請求管理者については個人宛のルールが必要ないのでグループを作らない

  # 開発者の権限を管理するロール
  IAMRoleDeveloper:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: 'developer'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS:
                - !GetAtt IAMUserDev1.Arn
                - !GetAtt IAMUserProd1.Arn
                - !GetAtt IAMUserProd2.Arn
            Action: 'sts:AssumeRole'
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/ReadOnlyAccess'

  # 本番運用者の権限を管理するロール
  IAMRoleProductionOperator:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: 'production-operator'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS:
                - !GetAtt IAMUserProd1.Arn
                - !GetAtt IAMUserProd2.Arn
            Action: 'sts:AssumeRole'
            Condition:
              Bool:
                aws:MultiFactorAuthPresent: true
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/ReadOnlyAccess'
        - 'arn:aws:iam::aws:policy/AmazonS3FullAccess'

  # 請求管理者の権限を管理するロール
  IAMRoleBillingOwner:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: 'billing-owner'
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              AWS:
                - !GetAtt IAMUserBilling1.Arn
            Action: 'sts:AssumeRole'
            Condition:
              Bool:
                aws:MultiFactorAuthPresent: true
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/job-function/Billing'

  IAMUserDev1:
    Type: 'AWS::IAM::User'
    Properties:
      LoginProfile:
        Password: 'i_love_whisky'
        PasswordResetRequired: true
      UserName: !Ref Dev1

  IAMUserProd1:
    Type: 'AWS::IAM::User'
    Properties:
      LoginProfile:
        Password: 'i_love_cigar'
        PasswordResetRequired: true
      UserName: !Ref Prod1

  IAMUserProd2:
    Type: 'AWS::IAM::User'
    Properties:
      LoginProfile:
        Password: 'i_love_sleeping'
        PasswordResetRequired: true
      UserName: !Ref Prod2

  IAMUserBilling1:
    Type: 'AWS::IAM::User'
    Properties:
      LoginProfile:
        Password: 'i_love_money'
        PasswordResetRequired: true
      UserName: !Ref Billing1

  IAMUserToGroupAdditionAllMember:
    Type: 'AWS::IAM::UserToGroupAddition'
    Properties:
      GroupName: !Ref IAMGroupAllMember
      Users:
        - !Ref IAMUserDev1
        - !Ref IAMUserProd1
        - !Ref IAMUserProd2
        - !Ref IAMUserBilling1

  IAMUserToGroupAdditionProductionOperator:
    Type: 'AWS::IAM::UserToGroupAddition'
    Properties:
      GroupName: !Ref IAMGroupAllMember
      Users:
        - !Ref IAMUserProd1
        - !Ref IAMUserProd2

権限の補足

IAMグループの使いどころ

権限は次の方針で設定しています。

  • ユーザ自体には何の権限も与えない
  • IAMロールで権限管理

しかし、幾つかの権限はロール管理が難しいです。

  • 自分のパスワードの変更

    • 特に、初ログイン時のパスワード変更はユーザ権限での操作以外ムリ
  • 自分のMFA設定
  • 自分のSSH鍵管理
    • OpsWorksを使う場合

「自分の」という部分が曲者です。単に権限を与えるだけでなく、「自分だけできる」というのが重要です。
そのため、これら「自分の情報だけに対して更新権限を与えたい」という場合はIAMグループを使って権限付与しています。

              - Effect: 'Allow'
                Action:
                  - 'iam:GetLoginProfile'
                  - 'iam:ChangePassword'
                  - 'iam:CreateVirtualMFADevice'
                  - 'iam:DeleteVirtualMFADevice'
                  - 'iam:EnableMFADevice'
                Resource: 
                  - !Sub 'arn:aws:iam::${AWS::AccountId}:mfa/${!aws:username}' # 自分のMFAだけを対象に
                  - !Sub 'arn:aws:iam::${AWS::AccountId}:user/${!aws:username} # 自分のユーザ情報だけを対象に

OpsWorksはActionレベルで自分だけに対してのものを用意してくれているのでそれを使います。

              - Effect: 'Allow'
                Action:
                  - 'opsworks:UpdateMyUserProfile' # 自分の情報だけの更新権限, 参照権限版もある(DescribeMyUserProfile)
                  - 'opsworks:Describe*'
                Resource: '*'

AssumeRoleの条件

AssumeRoleは、ユーザの指定以外にも特定の条件を満たした場合のみ、という制約をつけることができます。

本番運用者と請求管理者にはユーザの指定以外にMFAでログインしている場合のみAssumeRoleを許可するという制約をつけました。

      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              AWS:
                - !GetAtt IAMUserBilling1.Arn
            Action: 'sts:AssumeRole'
            Condition:
              Bool:
                aws:MultiFactorAuthPresent: true # MFAログインでないとConditionが満たせずAssumeRoleできない

さいごに

AssumeRoleを理解してからは権限管理の世界が広がった感があります。
ぜひ権限を追加して、実態に即したロールに育ててください。

さいごのさいごに

(IAMの具体的な使い方を紹介する記事がもっと増えますように)

続きを読む

CloudFormationとecs-cliを使って「Fargate+NLB+fluentd」環境を作成する。

AWS Fargateが発表されたのでさっそくつかってみます。
今回はFargateの上でfluentdコンテナを実行します。

手順

  1. CloudFromationのテンプレートでECSクラスタやNLBなどのAWSリソースを作ります。
  2. fluentdのDockerfiledocker-compose.ymlecs-params.ymlを作ります。
  3. fluentdのDockerイメージを作成し、ECRにpushします。
  4. ecs-cliコマンドでFargateにデプロイします。
  5. ecs-cli logsコマンドで接続を受け付けていることを確認します。

CloudFormationテンプレート

  • VPCから一気に作ります。
AWSTemplateFormatVersion: '2010-09-09'

Description: ""

Parameters:
  VPCCidr:
    Description: ""
    Type: String
    Default: "10.1.0.0/16"
  EnableDnsSupport:
    Description: ""
    Type: String
    Default: true
    AllowedValues:
      - true
      - false
  EnableDnsHostnames:
    Description: ""
    Type: String
    Default: true
    AllowedValues:
      - true
      - false
  InstanceTenancy:
    Description: ""
    Type: String
    Default: default
    AllowedValues:
      - default
      - dedicated
  SubnetCidrA:
    Description: ""
    Type: String
    Default: "10.1.10.0/24"
  SubnetCidrB:
    Description: ""
    Type: String
    Default: "10.1.20.0/24"
  AvailabilityZoneA:
    Description: ""
    Type: String
    Default: us-east-1a
  AvailabilityZoneB:
    Description: ""
    Type: String
    Default: us-east-1b
  MapPublicIpOnLaunch:
    Description: ""
    Type: String
    Default: true
    AllowedValues:
      - true
      - false
  AttachInternetGateway:
    Description: ""
    Type: String
    Default: "true"
    AllowedValues:
      - true
      - false
  Schema:
    Description: ""
    Type: String
    Default: "internet-facing"
    AllowedValues:
      - "internet-facing"
      - "internal"
  ListenerPort:
    Description: ""
    Type: String
    Default: "24224"
  TargetGroupPort:
    Description: ""
    Type: String
    Default: "24224"
  TargetType:
    Description: ""
    Type: String
    Default: "ip"
    AllowedValues:
      - "ip"
      - "instance"

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "Network Configuration"
        Parameters:
          - VPCCidr
          - EnableDnsSupport
          - EnableDnsHostnames
          - InstanceTenancy
          - SubnetCidrA
          - SubnetCidrB
          - AvailabilityZoneA
          - AvailabilityZoneB
          - MapPublicIpOnLaunch
          - AttachInternetGateway
      - Label:
          default: "NetworkLoadBalancer Configuration"
        Parameters:
          - Schema
          - ListenerPort
          - TargetGroupPort
          - TargetType

Outputs:
  Cluster:
    Value: !Ref Cluster
  TargetGroupArn:
    Value: !Ref TargetGroup
  SubnetA:
    Value: !Ref SubnetA
  SubnetB:
    Value: !Ref SubnetB
  SecurityGroup:
    Value: !GetAtt SecurityGroup.GroupId
  IAMRole:
    Value: !GetAtt IAMRole.Arn
  Repository:
    Value: !Ref Repository

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCidr
      EnableDnsSupport: !Ref EnableDnsSupport
      EnableDnsHostnames: !Ref EnableDnsHostnames
      InstanceTenancy: !Ref InstanceTenancy
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}.vpc"

  RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}.rtable"

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}.igw"

  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC

  Route:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  SubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref SubnetCidrA
      AvailabilityZone: !Ref AvailabilityZoneA
      MapPublicIpOnLaunch: !Ref MapPublicIpOnLaunch
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}.subnet"

  SubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetA
      RouteTableId: !Ref RouteTable

  SubnetB:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref SubnetCidrB
      AvailabilityZone: !Ref AvailabilityZoneB
      MapPublicIpOnLaunch: !Ref MapPublicIpOnLaunch
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}.subnet"

  SubnetBRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetB
      RouteTableId: !Ref RouteTable

  SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: "SecurityGroup"
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - CidrIp: !Ref VPCCidr
          IpProtocol: "tcp"
          FromPort: "24224"
          ToPort: "24224"
        - CidrIp: 0.0.0.0/0
          IpProtocol: "tcp"
          FromPort: "24224"
          ToPort: "24224"
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}.sg"

  Cluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Ref AWS::StackName

  LoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: !Sub "${AWS::StackName}-nlb"
      Type: "network"
      Scheme: !Ref Schema
      Subnets:
        - !Ref SubnetA
        - !Ref SubnetB
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}.nlb"

  Listener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref LoadBalancer
      Port: !Ref ListenerPort
      Protocol: "TCP"
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroup

  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    DependsOn: LoadBalancer
    Properties:
      Name: !Sub "${AWS::StackName}-tg"
      Port: !Ref TargetGroupPort
      Protocol: "TCP"
      VpcId: !Ref VPC
      TargetType: !Ref TargetType
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}.targetgroup"

  IAMRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: !Sub "${AWS::StackName}.role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action:
              - "sts:AssumeRole"
            Principal:
              Service:
                - "ecs-tasks.amazonaws.com"
      Policies:
        - PolicyName: !Sub "${AWS::StackName}.policy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Resource: "*"
                Action:
                  - "ecr:GetAuthorizationToken"
                  - "ecr:BatchCheckLayerAvailability"
                  - "ecr:GetDownloadUrlForLayer"
                  - "ecr:BatchGetImage"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"

  LogGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Sub "${AWS::StackName}/fluentd"
      RetentionInDays: 3

  Repository:
    Type: "AWS::ECR::Repository"
    Properties:
      RepositoryName: !Sub "${AWS::StackName}/fluentd"
  • ポイント

    1. NLBにはSecurityGroupはアタッチできない。
    2. コンテナにはVPC Cidrからのアクセスを許可する必要がある。これがないとヘルスチェックが失敗する。
    3. コンテナはロググループを自動生成しない。あらかじめロググループを作成しておく必要がある。

Stackの作成

  • AWSコンソールからCloudFormationを選択してStackを作成します。

cfn

  • しばらく待ちます。

creating

  • 完成です。

created

コンテナの設定

  • 以下のディレクトリ構成で設定ファイルを書きます。
├── Makefile
└── fluentd
    ├── Dockerfile
    ├── docker-compose.yml
    ├── ecs-params.yml
    ├── fluent.conf
    └── plugins //今回は空ディレクトリでok

fluent.conf

  • 標準出力に吐き出すだけです。
  • CloudWatchLogsに転送されます。
<source>
  type forward
  bind 0.0.0.0
  port 24224
</source>

<match **>
  type stdout
</match>

Dockerfile

FROM fluent/fluentd
COPY fluent.conf /fluentd/etc

docker-compose.yml

  • ${AWS::Account}はAWSアカウントID、${AWS::StackName}はCloudFormationで作成したスタックの名前を入れます。
version: '2'
services:
  fluentd:
    image: ${AWS::Account}.dkr.ecr.us-east-1.amazonaws.com/${AWS::StackName}/fluentd
    ports:
      - "24224:24224"
    logging:
      driver: "awslogs"
      options:
        awslogs-region: "us-east-1"
        awslogs-group: "${AWS::StackName}/fluentd"
        awslogs-stream-prefix: "container"

ecs-params.yml

  • ${SubnetA}, ${SubnetB}, ${SecurityGroup}はCloudFormationで作成したリソースのIDを入れます。
  • CloudFormationの「出力」タブに作成したリソースの一覧が表示されているのでコピペします。
  • 自動生成してほしい。
version: 1
task_definition:
  ecs_network_mode: awsvpc
  task_execution_role: arn:aws:iam::${AWS::Account}:role/${AWS::StackName}.role
  task_size:
    cpu_limit: 0.25
    mem_limit: 0.5GB
  services:
    fluentd:
      essential: true

run_params:
  network_configuration:
    awsvpc_configuration:
      subnets:
        - ${SubnetA}
        - ${SubnetB}
      security_groups:
        - ${SecurityGroup}
      assign_public_ip: ENABLED
  • この設定ファイルをみて分かる通り、Fargate(正確にはawsvpcモード)ではコンテナに直接サブネットやセキュリティグループをアタッチします。
  • EC2インスタンスのような感覚でコンテナを扱えます。

Makefile

  • たくさんコマンドをうつのでMakefileをつくっておきます。
push:
    docker build -f fluentd/Dockerfile -t ${AWS::Account}.dkr.ecr.us-east-1.amazonaws.com/${AWS::StackName}/fluentd fluentd
    `aws ecr get-login --no-include-email --region us-east-1`
    docker push ${AWS::Account}.dkr.ecr.us-east-1.amazonaws.com/${AWS::StackName}/fluentd:latest

up:
    cd fluentd; 
    ecs-cli compose service up 
        --cluster ${AWS::StackName} 
        --target-group-arn ${TargetGroupArn} 
        --launch-type FARGATE 
        --container-name fluentd 
        --container-port 24224 
        --region us-east-1 
        --timeout 10

rm:
    cd fluentd; 
    ecs-cli compose service rm 
    --cluster ${AWS::StackName} 
    --region us-east-1 
    --timeout 10

Deploy

  • イメージを作ってECRにpushします。
$ make push
docker build -f fluentd/Dockerfile -t ************.dkr.ecr.us-east-1.amazonaws.com/fargate/fluentd fluentd
Sending build context to Docker daemon  5.632kB
Step 1/2 : FROM fluent/fluentd
 ---> 060874232311
Step 2/2 : COPY fluent.conf /fluentd/etc
 ---> Using cache
 ---> 1ee08befeb8d
Successfully built 1ee08befeb8d
Successfully tagged ************.dkr.ecr.us-east-1.amazonaws.com/fargate/fluentd:latest
`aws ecr get-login --no-include-email --region us-east-1`
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
Login Succeeded
docker push ************.dkr.ecr.us-east-1.amazonaws.com/fargate/fluentd:latest
The push refers to a repository [************.dkr.ecr.us-east-1.amazonaws.com/fargate/fluentd]
baa346e06fe3: Pushed 
fe129fa31f70: Pushed 
dcf88bef8f3a: Pushed 
b59190601542: Pushed 
56e1e5a28df0: Pushed 
9fc62b353b50: Pushed 
26fbe6ae586e: Pushed 
16174e87921f: Pushed 
latest: digest: sha256:9f8c90b5fc10c084f93c5a93c038f4d307676b4fb641a8a36d67f4573655d52f size: 1981
  • Fargateにデプロイします。
$ make up
cd fluentd; 
    ecs-cli compose service up 
    --cluster fargate 
    --target-group-arn arn:aws:elasticloadbalancing:us-east-1:************:targetgroup/fargate-tg/**** 
    --launch-type FARGATE 
    --container-name fluentd 
    --container-port 24224 
    --region us-east-1 
    --timeout 10
WARN[0000] Skipping unsupported YAML option...           option name=networks
WARN[0000] Skipping unsupported YAML option for service...  option name=networks service name=fluentd
INFO[0001] Using ECS task definition                     TaskDefinition="fluentd:12"
INFO[0002] Created an ECS service                        service=fluentd taskDefinition="fluentd:12"
INFO[0002] Updated ECS service successfully              desiredCount=1 serviceName=fluentd
INFO[0017] (service fluentd) has started 1 tasks: (task 7228958b-0de1-4e31-a6b2-52d35b6c7b84).  timestamp=2017-12-07 02:14:52 +0000 UTC
INFO[0139] Service status                                desiredCount=1 runningCount=1 serviceName=fluentd
INFO[0139] ECS Service has reached a stable state        desiredCount=1 runningCount=1 serviceName=fluentd

確認

  • ログを取得します。
$ ecs-cli ps --cluster fargate --region us-east-1
Name                                          State                Ports                         TaskDefinition
7228958b-0de1-4e31-a6b2-52d35b6c7b84/fluentd  RUNNING              **.**.**.**:24224->24224/tcp  fluentd:12

$ ecs-cli logs --cluster fargate --region us-east-1 --task-id 7228958b-0de1-4e31-a6b2-52d35b6c7b84
2017-12-07 02:17:03 +0000 [info]: reading config file path="/fluentd/etc/fluent.conf"
2017-12-07 02:17:03 +0000 [info]: starting fluentd-0.12.40
2017-12-07 02:17:03 +0000 [info]: gem 'fluentd' version '0.12.40'
2017-12-07 02:17:03 +0000 [info]: adding match pattern="**" type="stdout"
2017-12-07 02:17:03 +0000 [info]: adding source type="forward"
2017-12-07 02:17:03 +0000 [info]: using configuration file: <ROOT>
  <source>
    type forward
    bind 0.0.0.0
    port 24224
  </source>

  <match **>
    type stdout
  </match>
</ROOT>
2017-12-07 02:17:03 +0000 [info]: listening fluent socket on 0.0.0.0:24224
  • 正常に起動しています。
  • fluent-cattelnetで接続するとログが出力されます。

削除

  • 作成したリソースを消しておかないとお金を取られます。
  • コンテナを削除してから、AWSリソースを削除します。
$ make rm
cd fluentd; 
    ecs-cli compose service rm 
    --cluster fargate 
    --region us-east-1 
    --timeout 10
WARN[0000] Skipping unsupported YAML option...           option name=networks
WARN[0000] Skipping unsupported YAML option for service...  option name=networks service name=fluentd
INFO[0001] Updated ECS service successfully              desiredCount=0 serviceName=fluentd
INFO[0001] Service status                                desiredCount=0 runningCount=1 serviceName=fluentd
INFO[0017] (service fluentd) has begun draining connections on 1 tasks.  timestamp=2017-12-07 02:44:53 +0000 UTC
INFO[0017] (service fluentd) deregistered 1 targets in (target-group arn:aws:elasticloadbalancing:us-east-1:************:targetgroup/fargate-tg/****)  timestamp=2017-12-07 02:44:53 +0000 UTC
INFO[0321] Service status                                desiredCount=0 runningCount=0 serviceName=fluentd
INFO[0321] ECS Service has reached a stable state        desiredCount=0 runningCount=0 serviceName=fluentd
INFO[0321] Deleted ECS service                           service=fluentd
INFO[0322] ECS Service has reached a stable state        desiredCount=0 runningCount=0 serviceName=fluentd
  • AWSコンソールでECRリポジトリを削除します。

    • CloudFromationで作成したリポジトリにイメージが登録されていると、CloudFormationでは削除できません。
  • AWSコンソール -> CloudFromation -> Stackの削除をします。

使ってみた感想

今まではECSクラスタを構成するEC2インスタンスの制限を受けて使いにくい部分(動的ポートマッピングとかawsvpcのeni制限とかスケールとか)がありましたが、Fargateによってそれらが一気に解決しました。AWSでコンテナ運用するならFargate一択ですね。

参考

  1. AWS Fargate: サービス概要
  2. GitHub – aws/amazon-ecs-cli

続きを読む

AWS CloudFormationを使ってAWS Fargateの環境を作成してみる

本記事は個人の意見であり、所属する組織の見解とは関係ありません。

こちらはAWS Fargate Advent Calendar 2017の6日目の記事です。
AWS Fargateが発表されて、一週間ぐらい経ちました。新しいサービス、機能を色々試してみるのは楽しいですよね!

今日は、Fargateを触ってみて、もう少し本格的に取り組んでみたいと感じた方向けにAWS CloudFormationを使ってAWS Fargateの環境を作成する流れについて確認してみたいと思います。

AWS CloudFormationとは

Cloudformationでは、AWSリソースの環境構築を設定テンプレートを元に自動化する事ができます。ECSで利用する場合、TaskdefinisionやServiceの設定なども記述する事ができます。Containerのデプロイをより簡単に行える様になり各種自動化を行いやすくなるメリットもあります。

今回はFargateのAdvent Calendarへの投稿ですので、詳細については、次のWebinerの資料を確認してみてください。

CloudFormationテンプレート作成

作成方針

Cloudformationのテンプレートは記載の自由度が高く、色々な記述の仕方ができるのですが、今回は分かりやすさを重視して次の様な構成で分割したテンプレートを作成してみました。

  • VPC作成用テンプレート

    • Fargate用のVPCを作成し、VPCの設定を行うテンプレート
    • PublicSubnetやPrivateSubnet、ルートテーブルなどを作成していきます。
  • SecurityGroup作成用テンプレート

    • TaskやALBで利用するSecurityGroupを作成します。
  • ECSクラスターを作成するテンプレート

  • ELBを設定するテンプレート

  • TaskDefinitionテンプレート

    • ECS上で起動するContainerに関する設定を行います。
  • Serviceテンプレート

分割の仕方も様々ですので、各自のユースケースにあわせて、色々と試してみてください。個人的には、ライフサイクルが異なるリソースは別テンプレートにするが好きです。逆に、開発環境やデモ環境を素早く立ち上げたい場合は1つのテンプレートの中に全て記載してしまうのもいいですよね。

VPC作成用テンプレート

テンプレートの一部は次の様な形になります。(折りたたんでいます)

Resources:
    VPC:
        Type: AWS::EC2::VPC
        Properties:
            CidrBlock: !Ref VpcCIDR
            Tags:
                - Key: Name
                  Value: !Ref EnvironmentName

    InternetGateway:
        Type: AWS::EC2::InternetGateway
        Properties:
            Tags:
                - Key: Name
                  Value: !Ref EnvironmentName


  <省略>


Outputs:

    VPC:
        Description: A reference to the created VPC
        Value: !Ref VPC
        Export:
          Name: !Sub ${EnvironmentName}-VPC

ポイントは、作成したリソースに関する情報を別リソースからもアクセスできる様に
OutpusセクションでExport属性をつけている事です。Export属性で定義しているNameを利用して、
別テンプレートからも対象リソースに対する参照を行う事ができます。

SecurityGroup作成用テンプレート

ALB用、Container用のSecurityGroupを作成し、必要なPortを許可しています。

こちらも長いので、折りたたんでいます。

Description: >
    This template deploys a security-groups.

Parameters:
  EnvironmentName:
    Description: An environment name that will be prefixed to resource names
    Type: String
    Default: advent-calendar-2017

Resources:
  LoadBalancerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !ImportValue advent-calendar-2017-VPC
      GroupDescription: SecurityGroup for ALB
      SecurityGroupIngress:
        - CidrIp: 0.0.0.0/0
          IpProtocol: tcp
          FromPort: 80
          ToPort: 80
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-LoadBalancers


  ContainerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !ImportValue advent-calendar-2017-VPC
      GroupDescription: Security Group for Task
      SecurityGroupIngress:
        -
          SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup
          IpProtocol: -1
        -
          CidrIp: 0.0.0.0/0
          IpProtocol: tcp
          FromPort: 80
          ToPort: 80

      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-ContainerSecurityGroup

Outputs:
  LoadBalancerSecurityGroup:
    Description: A reference to the security group for load balancers
    Value: !Ref LoadBalancerSecurityGroup
    Export:
      Name: !Sub ${EnvironmentName}-LoadBalancerSecurityGroup

  ContainerSecurityGroup:
    Description: A reference to the security group for EC2 hosts
    Value: !Ref ContainerSecurityGroup
    Export:
      Name: !Sub ${EnvironmentName}-ContainerSecurityGroup

ECSクラスタ-を作成するテンプレート

非常にシンプルです。ただ、クラスタを定義して名前をつけるだけ。

Description: >
    This sample template deploys a ECS Cluster

Parameters:
  EnvironmentName:
    Description: An environment name that will be prefixed to resource names
    Type: String
    Default: advent-calendar-2017

Resources:
  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Sub ${EnvironmentName}-cluster

Outputs:
  ECSCluster:
    Description: A referenc
    Value: !Ref ECSCluster
    Export:
      Name: !Sub ${EnvironmentName}-Cluster

ELBを設定するテンプレート

ALB、Listener、Targetgroupを作成しています。

Description: >
    This template deploys an Application Load Balancer.


Parameters:
  EnvironmentName:
    Description: An environment name that will be prefixed to resource names
    Type: String
    Default: advent-calendar-2017


Resources:
  LoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: !Ref EnvironmentName
      Subnets:
        - !ImportValue advent-calendar-2017-PublicSubnet1
        - !ImportValue advent-calendar-2017-PublicSubnet2
      SecurityGroups:
        - !ImportValue advent-calendar-2017-LoadBalancerSecurityGroup
      Tags:
        - Key: Name
          Value: !Ref EnvironmentName
      Scheme: internet-facing

  LoadBalancerListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref LoadBalancer
      Port: 80
      Protocol: HTTP
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref DefaultTargetGroup

  DefaultTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: !Sub ${EnvironmentName}-targetgroup
      VpcId: !ImportValue advent-calendar-2017-VPC
      Port: 80
      Protocol: HTTP
      TargetType: ip

Outputs:

  LoadBalancer:
    Description: A reference to the Application Load Balancer
    Value: !Ref LoadBalancer
    Export:
      Name: !Sub ${EnvironmentName}-Loadbalancer

  LoadBalancerUrl:
    Description: The URL of the ALB
    Value: !GetAtt LoadBalancer.DNSName

  Listener:
    Description: A reference to a port 80 listener
    Value: !Ref LoadBalancerListener
    Export:
      Name: !Sub ${EnvironmentName}-Listener

  DefaultTargetGroup:
    Value: !Ref DefaultTargetGroup
    Export:
      Name: !Sub ${EnvironmentName}-DefaultTargetGroup

TaskDefinition設定

ようやくFargateに関連する設定が出てきました。ここでは、RequiresCompatibilities属性にFARGATEを指定し、
NetworkMode属性にawsvpcを指定しています。また、CPU、メモリの設定はContainerDefinitionsの外側で設定します。
Container Definitionsにおけるmemory/cpuの指定はオプションです。加えて、各Taskがログを出力するためのCloudwatch Logsの設定もここで行なっています。

Description: >
    This sample deploys a task

Parameters:
  EnvironmentName:
    Description: An environment name that will be prefixed to resource names
    Type: String
    Default: advent-calendar-2017

Resources:
  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub '/ecs/logs/${EnvironmentName}-groups'

  ECSTask:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Cpu: 256
      ExecutionRoleArn: arn:aws:iam::XXXXXXXXXXXX:role/ecsTaskExecutionRole
      Family: !Sub ${EnvironmentName}-task
      Memory: 512
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      ContainerDefinitions:
        -
          Name: nginx
          Image: nginx:latest
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref LogGroup
              awslogs-region: us-east-1
              awslogs-stream-prefix: ecs
          MemoryReservation: 512
          PortMappings:
            -
              HostPort: 80
              Protocol: tcp
              ContainerPort: 80

Outputs:
  LogGroup:
      Description: A reference to LogGroup
      Value: !Ref LogGroup

  ECSTask:
    Description: A reference to Task
    Value: !Ref ECSTask

Service設定

ここではFargate上でTaskを起動させるために、LaunchType属性にFARGATEを指定しています。ここでTaskNameに指定しているXXの数字はTaskのRevisionに該当します。Taskの更新とともにここの数字を変える必要があるという点がポイントです。

Description: >
    This sample deploys a service

Parameters:
  EnvironmentName:
    Description: An environment name that will be prefixed to resource names
    Type: String
    Default: advent-calendar-2017

  TaskName:
    Description: A task name
    Type: String
    Default: advent-calendar-2017-task:XX

Resources:
  Service:
    Type: AWS::ECS::Service
    Properties:
      Cluster: !ImportValue advent-calendar-2017-Cluster
      DesiredCount: 2
      LaunchType: FARGATE
      LoadBalancers:
        -
          TargetGroupArn: !ImportValue advent-calendar-2017-DefaultTargetGroup
          ContainerPort: 80
          ContainerName: nginx
      NetworkConfiguration:
        AwsvpcConfiguration:
          SecurityGroups:
            -
              !ImportValue advent-calendar-2017-ContainerSecurityGroup
          Subnets:
            -
              !ImportValue advent-calendar-2017-PrivateSubnet1
            -
              !ImportValue advent-calendar-2017-PrivateSubnet2
      ServiceName: !Sub ${EnvironmentName}-service
      TaskDefinition: !Ref TaskName
Outputs:
  Service:
      Description: A reference to the service
      Value: !Ref Service

Cloudformation Stackを作成する

これで、Fargate環境の作成準備が整いました。ここからは順番にStackを作成していきます。

$ aws cloudformation create-stack --stack-name advent-calendar-2017-vpc 
--template-body file://Fargate-vpc.yml 
--region us-east-1

$ aws cloudformation create-stack --stack-name advent-calendar-2017-security-group 
--template-body file://Fargate-security-groups.yaml 
--region us-east-1

$ aws cloudformation create-stack --stack-name advent-calendar-2017-load-balancer 
--template-body file://Fargate-load-balancers.yaml 
--region us-east-1

$ aws cloudformation create-stack --stack-name advent-calendar-2017-cluster 
--template-body file://Fargate-cluster.yml 
--region us-east-1


$ aws cloudformation create-stack --stack-name advent-calendar-2017-task 
--template-body file://Fargate-taskdefinition.yml 
--region us-east-1

$ aws cloudformation create-stack --stack-name advent-calendar-2017-service 
--template-body file://Fargate-service.yml 
--region us-east-1

作成した環境を確認する

Cloudformationでの環境構築が終わりました。正しく構築できているか、ALB経由でアクセスして確認してみてください。
作成したALBのFQDNは、マネージメントコンソール上のEC2の画面>ロードバランサにアクセスして確認できます。
それ以外にも今回の例では、CLIでの次の様なコマンドで確認する事ができます。(少し無理やりですが。。。)

$ aws cloudformation describe-stacks --stack-name advent-calendar-2017-load-balancer  
--region us-east-1 | jq '.Stacks[].Outputs[] | select(.OutputKey == "LoadBalancerUrl")'

{
  "Description": "The URL of the ALB",
  "OutputKey": "LoadBalancerUrl",
  "OutputValue": "advent-calendar-2017-844241308.us-east-1.elb.amazonaws.com"
}


####awscli単独でやるなら、次の様にも書く事ができます。

aws cloudformation describe-stacks --stack-name advent-calendar-2017-load-balancer  
--region us-east-1 
--query 'Stacks[].Outputs[?OutputKey == `LoadBalancerUrl`].OutputValue'

ECSクラスター

次のコマンドで存在が確認できます。

$ aws ecs list-clusters --region us-east-1
{
    "clusterArns": [
        "arn:aws:ecs:us-east-1:925496135215:cluster/advent-calendar-2017-cluster"
    ]
}

サービスの状態

作成したサービスの状態は次の様なコマンドで確認できます。

aws ecs describe-services --services <service name> 
--cluster <cluster name> --region us-east-1

例えば、デプロイしているサービスの状況を確認する際には以下の様なコマンドで状態を取得可能です。
次のコマンド結果には、runningCountが2であり、desiredCountの設定通りにTaskが起動している事が確認できます。


$ aws ecs describe-services --services advent-calendar-2017-service 
--cluster advent-calendar-2017-cluster 
--region us-east-1 | jq .[][].deployments
[
  {
    "status": "PRIMARY",
    "networkConfiguration": {
      "awsvpcConfiguration": {
        "subnets": [
          "subnet-2541e678",
          "subnet-9297e0f6"
        ],
        "securityGroups": [
          "sg-326f1047"
        ]
      }
    },
    "pendingCount": 0,
    "createdAt": 1512499161.953,
    "desiredCount": 2,
    "taskDefinition": "arn:aws:ecs:us-east-1:XXXXXXXXXXXX:task-definition/advent-calendar-2017-task:3",
    "updatedAt": 1512500281.269,
    "id": "ecs-svc/9223370524355613851",
    "runningCount": 2
  }
]

デプロイしたServiceを更新する

Cloudformationを利用して作成していますので、更新もCloudformation経由で行います。

テンプレートを更新する。

今回はDesiredCountを修正してみました。

$ diff Fargate-service-update.yml Fargate-service.yml
20c20
<       DesiredCount: 4
---
>       DesiredCount: 2

Stackを更新する

次の様なコマンドでStackの更新が可能です。

$ aws cloudformation update-stack --stack-name advent-calendar-2017-service 
--template-body file://Fargate-service-update.yml 
--region us-east-1

しばらく待った後に再びServiceの状態を確認するとDsierdCount通りにTaskの数が増えている事が確認できます。

$ aws ecs describe-services --services advent-calendar-2017-service 
--cluster advent-calendar-2017-cluster 
--region us-east-1 | jq .[][].deployments
[
  {
    "status": "PRIMARY",
    "networkConfiguration": {
      "awsvpcConfiguration": {
        "subnets": [
          "subnet-2541e678",
          "subnet-9297e0f6"
        ],
        "securityGroups": [
          "sg-326f1047"
        ]
      }
    },
    "pendingCount": 0,
    "createdAt": 1512499161.953,
    "desiredCount": 4,
    "taskDefinition": "arn:aws:ecs:us-east-1:925496135215:task-definition/advent-calendar-2017-task:3",
    "updatedAt": 1512538215.582,
    "id": "ecs-svc/9223370524355613851",
    "runningCount": 4
  }
]

Taskをアップデートする。

テンプレートを更新する。

Taskが利用するメモリの容量を修正してみました。

$ diff Fargate-taskdefinition-update.yml Fargate-taskdefinition.yml 
25c25
<       Memory: 1024
---
>       Memory: 512

Stackを更新する

次の様なコマンドでTask用のStackの更新をします。

$ aws cloudformation update-stack --stack-name advent-calendar-2017-task 
--template-body file://Fargate-taskdefinition-update.yml 
--region us-east-1

TaskのRevisionが変化していますので、Serviceでも新しいRevisionを利用する様に、テンプレートを修正して、Service用のStackを更新します。

$ aws cloudformation update-stack --stack-name advent-calendar-2017-service 
--template-body file://Fargate-service.yml 
--region us-east-1

再度、サービスの状態を確認して、起動しているTaskが更新されている事を確認してみてください。

まとめ

Cloudformationを利用したECS,Fargateの操作はいかがだったでしょうか。今回の記事を書く為に、新規でCloudformationテンプレートを作成したのですが、これまでのECSで利用していたテンプレートとの違いは僅かでした。FargateをきっかけにECSに興味を持って頂けた方の参考になればうれしいです。

続きを読む

AWS Fargateを使用してNginxを起動した(ENI使用)

AWS Fargateとは

AWS re:Invent2017でAWSが発表したコンテナホスト(インスタンス)を意識せずにコンテナを管理できるオーケストレーションサービスです。
現時点ではバージニア(us-east-1)リージョンのみでGAとなっています。
ちなみに、re:Inventに参加したので、現地でキーノートを聞いていましたが、同時にEKSも発表され待望のサービスが来た!という感じで私も会場もめちゃくちゃ興奮してました。

やってみる

クラスタの作成

リージョンはバージニアを選択し、Elastic Container Serviceの画面を開きます。
クラスターの作成ボタンを押してクラスタの作成を始めます。
(私の環境は元々別のクラスタが作成してあったので初めての場合表示が違うかもしれません)
console_aws_amazon_com_ecs_home_region_us-east-1.png

クラスタテンプレートの選択

以下の3つのクラスタテンプレートが選択できます。

  • Network only
  • EC2 Linux + Networking
  • EC2 Windows + Networking

Network onlyがFargateのようなので、こちらを選択して次のステップへ進みます。
console_aws_amazon_com_ecs_home_region_us-east-1__1_.png

クラスタの設定

ここではクラスタ名とネットワークの設定をします。
クラスタ名はFargateTestにしてみました。
また、このクラスタ用にVPCも合わせて作ってくれるオプションがあったので選択しました。
値はデフォルトのままで作成
console_aws_amazon_com_ecs_home_region_us-east-1__2_.png

起動ステータスの確認

作成が始まるとCloudFormationスタックが走ります。
しばらくするとイイ感じにクラスタができあがります。
console_aws_amazon_com_ecs_home_region_us-east-1__3_.png

クラスタの状態を確認

当然ですが実行中のタスクもサービスもありません。
console_aws_amazon_com_ecs_home_region_us-east-1__4_.png

タスク定義作成

新しくNginxを起動するタスクを定義します。
(私の環境は元々別のタスクが登録してあったので初めての場合表示が違うかもしれません)
console_aws_amazon_com_ecs_home_region_us-east-1__5_.png

ローンチタイプの選択

FARGATEEC2が選択できるので、当然FARGATEを選んで次のステップに進みます。
console_aws_amazon_com_ecs_home_region_us-east-1__6_.png

タスク定義の設定

適当にタスク定義に名前をつけます。ここではTestNginxとしました。
タスクのサイズ指定でメモリとvCPUを選択できます。
メモリは0.5GBと1GB~30GBまで1GB刻みで調整できるようです。
CPUは0.25、0.5、1、2、4から選択します。
ここではどちらも最小値の0.5GB、0.25vCPUを選択しました。
続いてコンテナの追加を行います。
console_aws_amazon_com_ecs_home_region_us-east-1__7_.png

コンテナの追加

コンテナ名を適当に定義します。ここではTestNginxにしました。
また、使用するコンテナイメージを指定します。DockerHubで公開されているNginxイメージをそのまま指定しました。
Nginxコンテナはポート80でListenするのでポートマッピングの設定は80となります。
その他は特に変更せずに追加し、そのまま作成します。
console_aws_amazon_com_ecs_home_region_us-east-1__8_.png

ローンチステータスの確認

ECSタスクと必要なIAM Roleが定義されたようです。
console_aws_amazon_com_ecs_home_region_us-east-1__9_.png

サービスの作成

クラスタの一覧から今回作成したFargateTestを選択します。
console_aws_amazon_com_ecs_home_region_us-east-1__11_.png

サービスの作成

サービスタブ内から作成します。
console_aws_amazon_com_ecs_home_region_us-east-1__12_.png

サービスの設定

Launch Typeは当然FARGATEを選択します。
また、タスク定義は先程作成したTestNginxを指定します。
サービス名には適当な名前をつけます。ここではTestNginxとしました。
console_aws_amazon_com_ecs_home_region_us-east-1__13_.png

ネットワーク構成の設定

参加するVPCを指定します。たぶん最初に作ったVPC IDが指定されていると思います。
コンテナが起動するサブネットを追加します。AZごとに別れていると思います。
また、セキュリティグループを編集することで既存のセキュリティグループを指定したり新規作成の内容を微調整できるようです。デフォルトのままだとポート80をAnyで公開します。
今回はELBを使用せずに直接コンテナに接続してみたいのでELBタイプはなしを選択しました。
このまま次のステップに進みます。
console_aws_amazon_com_ecs_home_region_us-east-1__14_.png

オートスケールの設定

オートスケールの設定ができますが、今回は実施しないのでそのまま次のステップに進みます。
console_aws_amazon_com_ecs_home_region_us-east-1__15_.png

サービスの確認

内容を確認してサービスの作成をします。
console_aws_amazon_com_ecs_home_region_us-east-1__16_.png

作成ステータスの確認

セキュリティグループの設定やサービスの設定が進みます。
console_aws_amazon_com_ecs_home_region_us-east-1__17_.png

動作確認

タスクのステータス確認

クラスタ内のタスクタブでステータスがRUNNINGになっていることを確認します。少し時間がかかる場合があります。
起動中のタスクIDを選択して詳細を確認します。
console_aws_amazon_com_ecs_home_region_us-east-1__18_.png

タスクの詳細確認

Networkの部分を見ると、ENI idがアタッチされていることが確認できます。
コンテナは外部に対してこのENIで通信します。
ENI IDを選択するとENIの画面に遷移します。
console_aws_amazon_com_ecs_home_region_us-east-1__19_.png

ENIの確認

ENIの画面に遷移したら、IPv4パブリックIPの部分を参照します。
ここに表示されているIPアドレスでコンテナと通信できます。
us-east-1_console_aws_amazon_com_ec2_v2_home_region_us-east-1.png

Nginxにアクセス

ブラウザで先程確認したIPアドレスにアクセスしてみます。
スクリーンショット 2017-12-05 17.28.17.png
無事Nginxが起動していました。

後片付け

作成したクラスタを削除します。
…と言いたいところですが、単純に削除したところ途中でエラーが出ました。
サービスを作成した際にできたセキュリティグループが残ってしまうので、CloudFormationStackが削除できないことが原因でした。
おそらく正しい手順はサービスの削除 -> セキュリティグループの削除 -> クラスタの削除となります。
まさか最後にアンチパターンにぶつかるとは。。。
console_aws_amazon_com_ecs_home_region_us-east-1__20_.png

最後に

インスタンスを意識する必要がないので保守範囲が減り、期待通りかなり魅力あるサービスだと感じました。
運用面では監視に課題が出てきそうなのかなーと思いました。
なんにせよ、Terraformへの組み込みと、東京リージョンでのサービス開始が待ち遠しいです。

続きを読む

Cognito User Pool – ロールベースアクセス制御を CFn で構築する

Amazon Web Services Advent Calendar 20172日目です。大変遅くなってしまい、申し訳ありません…

前書き

re:Invent は今年も盛り上がりましたね。キーノート中ずっと喋りっぱなしの Andy Jassy の喉が心配になるほど大量の新サービスが発表されて、個人的には EKS, Fargate, Comprehend, Lambda の Go 対応あたりが興味深いと思いましたが、本記事では既存サービスについて書きます。

ちょうど1年前、2016年12月に Cognito にロールベースのアクセス制御機能が追加されました。

チュートリアルとしては上の記事の通りなのですが、 API の仕様理解と構築自動化のため、これをすべて CloudFormation で構築した上で簡単なデモを動かしてみます。

デモの仕様

こんな感じのものを作ってみました。

  • Cognito User Pool に登録されたユーザーの username/password でログインできる
  • ユーザー登録はここでは実装しない
  • ログインすると Cognito Identity が発行した temporaly credentials を使って S3 バケットに置いてあるオブジェクトにアクセスできる
    • ここでは固定された1つのオブジェクト CONTENT.md のみとする
  • ユーザーのカスタム属性 role の値に従って異なる IAM Role が assume され、 S3 バケットにどのようなアクセスができるかが変化する
logged in? user attribute: custom:role S3 (read) S3 (write)
No No No
Yes viewer Yes No
Yes editor Yes Yes

早速触ってみる

Angular で実装してみました!

デプロイ済みのデモはこちらです。そのコードはこちらです。

hello-viewer, hello-editor というユーザーでログインできます。どちらもパスワードは Password1234 です。

ログインすると、 View のページから S3 に置かれたオブジェクトの内容が見られます。せっかくなので Markdown をパースして表示しています。

hello-editor でログインした場合はこの内容を編集して更新できます。

また、 Identity のページでは、現在払い出されているロールを確認できます。ログイン前とログイン後、そしてログインしているユーザーによって異なるロールが払い出されていることを確認できると思います。

CFn による構築

ほとんど CloudFormation で Cognito で書いたままです。

後述する理由によりカスタムリソースが必要なので、 Serverless Framework で一式をデプロイすることにしました。 serverless.yml にゴリゴリとリソースを並べていきますserverless.yml が500行超えるやつは大体友達。

各リソースについて、この要件に必要な部分をざらっとおさらいします。

UserPool

ロールベースアクセス制御に使うカスタム属性を Schema で定義しています。今回は role というカスタム属性を追加しました。

なお、カスタム属性は必須ではなく、例えば email などデフォルトの属性を使ってロール割り当てを切り替えることも可能です。

UserPoolClient

今回はクライアントが Web なので、 GenerateSecretfalse です。

IdentityPool

特になし。 IdentityPoolName にハイフンを含めないのが地味に不便です。

CognitoUserBasePolicy, CognitoUserAuthenticatedPolicy

コンソールからポチポチして作ると付けられるポリシーを元に作ってます。

今回はポリシーについては authed/unauthed という分け方ではなく、差分を追加していくという方法で分けてみました。

UnauthenticatedRole, AuthenticatedRole

これもコンソールポチポチロールを元に作ってます。 AssumeRolePolicyDocument が複雑なのでがんばりましょう。

AuthenticatedRole には今回の要件で必要な S3 へのアクセス権限などは一切追加しません(重要)。

ViewablePolicy, EditablePolicy

S3 へのアクセス権をこちらで定義しました。

ViewerRole, EditorRole

上の ViewablePolicy, EditablePolicy を付けたロールです。これらが今回のデモでログインした時に払い出されます。

RoleAttachment

UnauthenticatedRole, AuthenticatedRole をここで IdentityPool に紐付けます。

本来であれば、このリソースの RoleMappingsViewerRole, EditorRoleも紐付けたいのですが、そうしていません。

なぜなら CloudFormation は KeyValue の Key を動的に定義できない という問題があるため、 RoleMappings のキー(cognito-idp.<region>.amazonaws.com/<userPool>:<userPoolClient> という形式)をどうやってもハードコードする必要が生じてしまうからです。

この問題はフォーラムでも話題に上がっています。

IdentityPoolRoleMapping

というわけで、上の問題を無理矢理に解決するためのカスタムリソースです。

cognito-identity:SetIdentityPoolRoles を行うには iam:PassRole の権限も必要です。地味にハマリポイント。

属性のマッチ条件は完全一致、部分一致、不一致、前方一致が選べるようです。後方一致があれば email のドメイン名で…ができて便利そうなのですが。

Bucket

ユーザーが編集できるオブジェクトの他に、今回はデモそれ自体の動作に必要なファイルもここに置きます。

ブラウザー上で動作する SDK から直接叩くので、 CORS 設定が必要です。

BucketPolicy

public/* のみ全開放とします。

AwsConfiguration

どうせカスタムリソースを使うならということで、 Cognito の認証に必要な情報も JSON にしてバケットに置いておきます。

こうすることでクライアントのコードに Cognito 関係のリソースの ID をハードコードしなくてよくなりますね。

AssetBundle

デモが動作するために必要なファイルをバケットに置くカスタムリソースです。

どうやって全自動でこれを置くか? と思案した結果、下のような流れになりました。

  1. ng build --prod でクライアントのコードをバンドルする
  2. serverless-webpackcopy-webpack-plugin を使い、クライアントのアセットバンドルを Lambda のバンドルの中に含める
  3. カスタムリソースでそれをバケットに置く

yarn run deploy を叩くとこの流れで一切合切がバケットに上がります。

その他

カスタムリソースを書くのが面倒すぎて、以前作った cfn-custom-resource-helper という npm パッケージを使っています。これの紹介はまたどこかで。

最後に

遅刻すみませんでした。

続きを読む

ゼロからはじめるServerless Java Container

日頃AWSやその他クラウドサービスを使ってインテグレーションしていく中で、 https://github.com/awslabs を定期的にウォッチしているのですが、その中で Serverless Java Container が気になったので試してみました。

https://github.com/awslabs/aws-serverless-java-container

Serverless Java Container is 何?

簡単に言うと、API GatewayとLambdaを使ったサーバレスアプリケーションを Jersey, Spark, Spring Frameworkといったフレームワークを使って作るためのライブラリです。

このライブラリを利用することで、「つなぎ」となる必要最低限のコードを書いてあげさえすれば、あとはいつも通り、フレームワークの流儀に沿ってアプリケーションを実装していくだけで、Lambda上で動くハンドラができあがる、というシロモノです。
絶対不可欠なライブラリではありませんが、あると便利なので、一考の価値はあると思います。


で、このAdvent Calendarにエントリした時には気づいていなかったのですが、AWSの中の人がこのライブラリについて詳しく紹介しているスライド・動画があることに気づきました :neutral_face:
「AWS Dev Day Tokyo 2017」で登壇された時のものみたいですね。

というわけで、このライブラリについての詳細な解説については上記を参考にしてもらうとして、今回のエントリでは、以下のような違いを出しつつ、このライブラリを使ってアプリケーションを作ってみることにします。

  • SAMやCloudFormationなどを使わずに、ゼロから構築してみる
  • よくあるPetStoreアプリケーションではなく、Hello, worldアプリケーションを作る
  • ビルドにはMavenではなくGradleを使う

試してみる

アプリケーションの雛形を作る

Spring Initialzrから新規にGradleプロジェクトを作っていきます。
必要な入力項目は以下のとおり。

項目 入力値
Group com.example
Artifact demo
Dependencies DevTools

「Generate Project」を押すと、プロジェクトの雛形がZipファイルで作られるので、展開後のディレクトリをワークスペースとします。

依存関係にServerless Java Containerを追加

今回の主役となるライブラリを追加します。執筆時点での最新バージョンは0.8のようでした。

build.gradle
    compile('com.amazonaws.serverless:aws-serverless-java-container-spring:0.8')

READMEにしたがってConfigとLambdaHandlerを作成

Serverless Java ContainerリポジトリのREADMEの「Spring support」のセクションを参考にして、アプリケーションとLambdaの「つなぎ」となる部分のコードを作っていきます。

まずはコンフィグ。

com.example.demo.AppConfig.java
@Configuration
@ComponentScan("com.example.demo")
public class AppConfig {
}

続いてハンドラ。

com.example.demo.LambdaHandler.java
public class LambdaHandler implements RequestHandler<AwsProxyRequest, AwsProxyResponse> {

    private static class Singleton {

        static SpringLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler = instance();

        static SpringLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> instance() {
            try {
                return SpringLambdaContainerHandler.getAwsProxyHandler(AppConfig.class);
            } catch (final ContainerInitializationException e) {
                throw new RuntimeException("Cannot get Spring Lambda Handler", e);
            }
        }
    }

    @Override
    public AwsProxyResponse handleRequest(AwsProxyRequest awsProxyRequest, Context context) {
        return Singleton.handler.proxy(awsProxyRequest, context);
    }
}

LambdaHandler の方は、READMEの通りに実装するとコンパイルエラーになってしまうので、少し修正しました。
SpringLambdaContainerHandler.getAwsProxyHandler がチェック例外 ContainerInitializationException を投げるので、そのままフィールドとして初期化できないんですよね…。

コントローラを作る

準備が終わったので、アプリケーション本体を作っていきます。
とは言え、今回は簡単なHello, Worldアプリケーションなので、これだけです。

com.example.demo.controller.DemoController.java
@RestController
public class DemoController {

    @GetMapping("/hello")
    public String hello(@RequestParam(required = false) Optional<String> message) {
        return "Hello, " + message.orElse("world") + "!";
    }
}

普通のSpringアプリケーションのコードですね。

Lambda用のパッケージの作成

下記ドキュメントを参考に、Lambda用のパッケージを作ります。
http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/create-deployment-pkg-zip-java.html

実は、普段Lambda関数を作る時はランタイムとしてNode.jsを使うことが多く、Javaランタイムを使うのは始めてでした。
build.gradle に以下を追加すればOKです。

build.gradle
task buildZip(type: Zip) {
    from compileJava
    from processResources
    into('lib') {
        from configurations.runtime
    }
}

build.dependsOn buildZip

Lambda関数を作ってパッケージをアップロード

Lambda関数を作って、パッケージをアップロードします。

ここで関数をテストする場合、イベントテンプレートとして「API Gateway AWS Proxy」を選択すればよいです。
選択して出てくるテンプレート中で、実装したアプリケーションに合わせて

  • "queryStringParameters""message": "好きな文字列"
  • "httpMethod""GET"
  • "path""/hello"

としてそれぞれ変更してください(下図)。

lambda.png

API Gatewayと連携させる

仕上げに、APIを作り、デプロイします。

今回は、ルートの直下にプロキシリソースを作ってしまいます(プロキシリソースの呼び出し先のLambda関数は、上記で作成したLambda関数を指定してください)

apigw2.png

テストの際は、クエリ文字列として message=好きな文字列(今回はServerless) を指定し、GETメソッドを呼び出すと、Springのコントローラが発火し、

Hello, Serverless!!

がレスポンスとして返ってくることが確認できます。

また、APIをデプロイした後は、curl コマンドなどでアクセスしても、きちんとレスポンスが取得できることが確認します。

$ curl "https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello?message=Serverless"

まとめ

繰り返しになってしまいますが、Serverless Java Containerライブラリ、簡単に言うと、API GatewayのLambdaプロキシ利用時に、普通のSpringアプリケーションとしてハンドラを実装できるようにするためのラッパー、という感じだと思います。
API Gatewayと組み合わせて力を発揮するライブラリですね。
最初は、Lambda上でSpringを使う時の足回りを面倒見てくれるライブラリかな?と思ったのですが、ちょっとイメージしていたものとはズレていました…。

現実的には、RDBに依存したアプリケーションの場合のコネクションの話1など、既存のWebアプリケーションがそのまま載せ替えられるか?というと検討ポイントはありそうですが、使い慣れたフレームワークをサーバレスアプリケーション化する場合には、こういったライブラリの活用もよいのかなーと思いました。

おまけ:起動時間など

LambdaのランタイムとしてJavaを使っている場合、起動時間も少し気になるところだと思うので、メモしておきます。

ちなみに、メモリの設定は512MBです。

正確なベンチマークは取得できていませんが、今回のアプリケーションで確認した範囲においては、10ミリ秒前後の処理時間で済むみたいです(コンテナが起動して、ApplicationContextが初期化済みになっている場合ですが)。

続きを読む

SSMパラメータをCloudFormationで使う

概要

CloudFormationのパラメータをSSMパラメータから取得する方法の解説

やってみる

コマンド

従来通り ParameterKeyParameterValue を渡せばOK
この時 ParameterValue にSSMパラメータのキー名を指定する

$ aws cloudformation update-stack 
    --stack-name <スタック名> 
    --template-url <テンプレートURL> 
    --parameters 
      ParameterKey=<CFnパラメーターキー>,ParameterValue=<SSMパラメータキー>
    --capabilities CAPABILITY_IAM

テンプレート

TypeAWS::SSM::Parameter::Value<String> 型にする。
コマンドとテンプレートを修正するだけでSSMパラメータからの呼び出しが可能に!

AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  <CFnパラメーターキー>:
    # コマンドの引数で指定した、<SSMパラメータキー>からパラメータを取得してくれる
    Type: AWS::SSM::Parameter::Value<String>

試す

MySQLを構築するテンプレートを例にする

SSMパラメータ(EC2 パラメータストア)

以下のキーで、DBユーザー名とパスワードをSSMパラメータに保管してあるとする。
SecureStringだとCloudFormationから呼び出せないので注意。

key value
MySQLUsername mysqluser
MySQLPassword mypassword

テンプレート

rds.yaml
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  Username:
    NoEcho: true
    Type: AWS::SSM::Parameter::Value<String>

  Password:
    NoEcho: true
    Type: AWS::SSM::Parameter::Value<String>

  StorageSize:
    Type: Number

Resources:
  DB: 
    Type: AWS::RDS::DBInstance
    Properties: 
      DBInstanceClass: "db.t2.medium"
      Engine: "MySQL"
      EngineVersion: "5.7"
      Iops: "1000"
      AllocatedStorage: !Ref StorageSize
      MasterUsername: !Ref: Username
      MasterUserPassword: !Ref Password

コマンド

ParameterValue を呼び出したいSSMパラメータのキー名にすることで呼び出せる

$ aws cloudformation update-stack 
    --stack-name rds-stack 
    --template-url file://rds.yaml 
    --parameters 
      ParameterKey=Username,ParameterValue=MySQLUsername 
      ParameterKey=Password,ParameterValue=MySQLPassword 
      ParameterKey=StorageSize,ParameterValue=100 
    --capabilities CAPABILITY_IAM

まとめ

機微情報の管理をSSMパラメータで一元管理できるのはとても良いですね

参考

続きを読む

CloudFormationで、ECSのCI/CD環境を構築した際のハマりどころ 〜CodePipeline,CodeBuild,KMSも添えて〜

Classiアドベントカレンダー4日目です。
本日は、ECSを利用して、AWS上でAWSどっぷりのCI/CD環境を準備したときのお話になります。

今年のre:InventでEKSとFargateがリリースされましたが、東京リージョンに来てなかったり、プレビュー段階だったりで、まだしばらくは参考になる部分はありそうかなと^^;

1.背景

などで、AWS公式でもECS環境下のCloudFormation(以下、CFn)を使ったデプロイ方法が紹介されています。
とはいえ、現実の要件でCFnで実装しようとすると、デフォルト設定だと失敗したり、ドキュメントだけだと、GUIで設定できる部分がCFnでの書き方がわからかったりして、いくつかハマった内容があったので、3種類ぐらいの特徴を抜粋して書いてみようと思います。

2.TL;DR

ECSを使うなら、

  • ALBとECSの動的ポート機能を組み合わせる
  • IAM Role,KMS,SSMパラメータストアを組み合わせる
  • CodePipelineで複数リポジトリからのコード取得を行う

これらの機能を全部CFnでやろうとすると、一部aws-cliなどを使う必要がありますが、
ひとまずDevとOpsでうまく権限を分担したCI/CD環境を構築できるのではないかなと思います。

3.特徴解説

3-1. ALBとECSの動的ポート機能の組み合わせ

qiita_ecs_port.png

EC2へ割り当てるSecurityGroupは、ECSの動的ポート機能を利用するため、インバウンドのTCPポートを開放しておきます。

securitygroup.yml
ALBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
        VpcId: !Ref VpcId
       GroupName: sample
       GroupDescription: "ALB Serurity Group"
       SecurityGroupIngress:
            -
                CidrIp: 0.0.0.0/0
                IpProtocol: tcp
                FromPort: 443
                ToPort: 443
EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
        VpcId: !Ref VpcId
       GroupName: sample
       GroupDescription: "EC2 Serurity Group"
       SecurityGroupIngress:
            -
                SourceSecurityGroupId: !Ref ALBSecurityGroup
                IpProtocol: tcp
                FromPort: 0
                ToPort: 65535

ECSの動的ポートを有効にするため、PortMappingsの設定でホストのポートを0に設定します。

ecs.yml
ECSTask:
    Type: "AWS::ECS::TaskDefinition"
    Properties:
        Family: sample
        NetworkMode: bridge
        ContainerDefinitions:
            -
                Name: sample
                Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRName}:${ImageTag}"
                Cpu: 2
                Memory: 128
                PortMappings:
                    -
                        ContainerPort: 80
                        HostPort: 0
                Essential: true
                Ulimits:
                    -
                        Name: nofile
                        SoftLimit: 65535
                        HardLimit: 65535
                Environment:
                    -
                        Name: TZ
                        Value: Asia/Tokyo
                LogConfiguration:
                    LogDriver: awslogs
                    Options:
                        awslogs-group: sample
                        awslogs-region: !Sub ${AWS::Region}
                        awslogs-stream-prefix: !Ref ImageTag
    Service:
        Type: "AWS::ECS::Service"
        Properties:
            ServiceName: sample
            Cluster: !Ref ECSCluster
            DesiredCount: 1
            TaskDefinition: !Ref ECSTask
            Role: !Ref ECSServiceRole
            PlacementStrategies:
                -
                    Type: spread
                    Field: instanceId
            LoadBalancers:
                -
                    ContainerName: sample
                    ContainerPort: 80
                    TargetGroupArn: !Ref ALBTargetGroup

注意点

複数のEC2でECSを運用するのであれば、PlacementStrategiesの設定を行っておかないと、random配置ECSのタスクが一つのホストだけに偏ってしまったりすることがあります。

3-2. DevとOpsで別gitリポジトリを運用しつつ、CodePipelineのデプロイフェーズでCFnのChangeSetを使う

qiita_codepipeline.png

デプロイにCFnを利用することで、デプロイの実行記録の管理やCFnで記載された部分のインフラ部分のテストを行いつつ、デプロイをすることが可能になります。
また、Sourceフェーズで、CFnの内容やEC2のASGやAMI設定の管理を行うOps管轄リポジトリと、Dockerコンテナ化するアプリロジックが含まれているDev管轄リポジトリを分割することで、
運用フェーズに入ったときにDevとOpsで独立して、デプロイを行うことができます。

codepipeline.yml
CodePipeline:
    Type: "AWS::CodePipeline::Pipeline"
    Properties:
        Name: sample
        ArtifactStore:
            Type: S3
            Location: sample
        RoleArn: !Ref BuildRole
        Stages:
            -
                Name: Source
                Actions:
                    -
                        Name: AppSource
                        RunOrder: 1
                        ActionTypeId:
                            Category: Source
                            Owner: ThirdParty
                            Version: 1
                            Provider: GitHub
                        Configuration:
                            Owner: !Ref GithubOwner
                            Repo: !Ref GithubAppRepo
                            Branch: !Ref GithubAppBranch
                            OAuthToken: !Ref GithubToken
                        OutputArtifacts:
                            - Name: AppSource
                    -
                        Name: InfraSource
                        RunOrder: 1
                        ActionTypeId:
                            Category: Source
                            Owner: ThirdParty
                            Version: 1
                            Provider: GitHub
                        Configuration:
                            Owner: !Ref GithubOwner
                            Repo: !Ref GithubInfraRepo
                            Branch: !Ref GithubInfraBranch
                            OAuthToken: !Ref GithubToken
                        OutputArtifacts:
                            - Name: InfraSource
            -
                Name: Build
                Actions:
                    -
                        Name: CodeBuild
                        RunOrder: 1
                        InputArtifacts:
                            - Name: AppSource
                        ActionTypeId:
                            Category: Build
                            Owner: AWS
                            Version: 1
                            Provider: CodeBuild
                        Configuration:
                            ProjectName: !Ref CodeBuild
                        OutputArtifacts:
                            - Name: Build
            -
                Name: CreateChangeSet
                Actions:
                    -
                        Name: CreateChangeSet
                        RunOrder: 1
                        InputArtifacts:
                            - Name: InfraSource
                            - Name: Build
                        ActionTypeId:
                            Category: Deploy
                            Owner: AWS
                            Version: 1
                            Provider: CloudFormation
                        Configuration:
                            ChangeSetName: Deploy
                            ActionMode: CHANGE_SET_REPLACE
                            StackName: !Sub ${AWS::StackName}
                            Capabilities: CAPABILITY_NAMED_IAM
                            TemplatePath: !Sub "Source::sample.yml"
                            ChangeSetName: !Ref CFnChangeSetName
                            RoleArn: !Ref BuildRole
                            ParameterOverrides: !Sub |
                                {
                                    "ImageTag": { "Fn::GetParam" : [ "Build", "build.json", "tag" ] },
                                    "AppName": "${AppName}",
                                    "OwnerName": "${OwnerName}",
                                    "RoleName": "${RoleName}",
                                    "StageName": "${StageName}",
                                    "VpcId": "${VpcId}"
                                }
            -
                Name: Deploy
                Actions:
                    -
                        Name: Deploy
                        ActionTypeId:
                            Category: Deploy
                            Owner: AWS
                            Version: 1
                            Provider: CloudFormation
                        Configuration:
                            ActionMode: CHANGE_SET_EXECUTE
                            ChangeSetName: !Ref CFnChangeSetName
                            RoleArn: !Ref BuildRole
                            StackName: !Sub ${AWS::StackName}

注意点

  • CodePipelineのキックは、PRがマージされたタイミングなので、(一応、CodePipelineにはTestフェーズもあるが)マージ前のテストなどはCircleCIとかに任せた方がよいかも
  • ParameterOverridesで上書きするパラメータは、CFnのParametersに設定している項目に応じて設定する
  • Sourceフェーズで持ってこれるリポジトリは2つまで。コンテナビルドに持ってくるのがもっとある場合、CodeBuild内でこちらの記事のように、githubから引っ張ってきて、ビルドするなどの対応が必要になりそう

3-3. CodeBuildでDockerイメージを作る際、KMSとSSMパラメータストアを利用する

qiita_codebuild.png

このあたりはAWSの恩恵をフルに受けている部分かなと。
RDSのパスワードや秘密鍵など、gitリポジトリ内で管理したくない情報は、SSMパラメータストアを使って、Dockerイメージを作成するときに環境変数を埋め込みます。

codebuild.yml
CodeBuild:
    Type: AWS::CodeBuild::Project
    Properties:
        Name: sample
        Source:
            Type: CODEPIPELINE
        ServiceRole: !Ref BuildRole
        Artifacts:
            Type: CODEPIPELINE
        Environment:
            Type: LINUX_CONTAINER
            ComputeType: BUILD_GENERAL1_SMALL
            Image: "aws/codebuild/docker:1.12.1"
            EnvironmentVariables:
                -
                    Name: AWS_DEFAULT_REGION
                    Value: !Sub ${AWS::Region}
                -
                    Name: AWS_ACCOUNT_ID
                    Value: !Sub ${AWS::AccountId}
                -
                    Name: IMAGE_REPO_NAME
                    Value: !Ref ECRRepoName

docker buildするときに、--build-argに秘匿情報として環境変数を引き渡し、できあがったイメージをECRにpushする。

buildspec.yml
version: 0.2

phases:
    pre_build:
        commands:
            - $(aws ecr get-login --region $AWS_DEFAULT_REGION)
            - IMAGE_TAG="${CODEBUILD_RESOLVED_SOURCE_VERSION}"
            - DB_PASSWORD=$(aws ssm get-parameters --names rds_pass --with-decryption --query "Parameters[0].Value" --output text)
    build:
        commands:
            - docker build --build-arg DB_PASSWORD="${DB_PASSWORD}" -t "${IMAGE_REPO_NAME}:${IMAGE_TAG}" .
            - docker tag "${IMAGE_REPO_NAME}:${IMAGE_TAG}" "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_REPO_NAME}:${IMAGE_TAG}"
    post_build:
        commands:
            - docker push "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_REPO_NAME}:${IMAGE_TAG}"
            - printf '{"tag":"%s"}' "${IMAGE_TAG}" > build.json
artifacts:
    files:
        - build.json
    discard-paths: yes
(snip)
ARG DB_PASSWORD
ENV DB_PASSWORD=${DB_PASSWORD}
(snip)

実運用する際は、IAM Roleを使う権限も意識して、KMSのKeyを利用するIAM UserやIAM Roleを設定する。

kms.yml
KMSKey:
    Type: "AWS::KMS::Key"
    Properties:
        Description: sample-key
        KeyPolicy:
            Version: "2012-10-17"
            Id: "key-default-1"
            Statement:
                -
                    Sid: "Allow use of the key"
                    Effect: "Allow"
                    Principal:
                        AWS: !GetAtt BuildRole.Arn
                    Action:
                        - "kms:DescribeKey"
                        - "kms:Decrypt"
                    Resource: "*"

注意点

  • SSMパラメータにおける、SecureString型の値登録
    3-3.でSSMパラメータストアで暗号化する際、SecureString型はCFnに対応していない。
    そのため、aws-cliで設定することにした。TerraformはSecureString型に対応しているので、CFn側でも対応して欲しいところ…
$ aws ssm put-parameter --name rds-pass --value PASSWORD --type SecureString --key-id hogehoge

4. その他の雑多なハマりどころ

4-1. ECSのAMIのデフォルト設定

  • EBSのストレージタイプのデフォルトがHDD
    LaunchConfigurationのBlockDeviceMappingsで、gp2を明示的に指定してあげる。
  • WillReplace用のシグナルを送るcfn-signalが未インストール
    UserDataの中で記載しておく。シグナルを送るタイミングは、どこまでAMIに手を入れるかによって変更する。
LaunchConfig:
    Type: "AWS::AutoScaling::LaunchConfiguration"
    Properties:
        AssociatePublicIpAddress: true
        KeyName: sample
        IamInstanceProfile: sample
        ImageId: ami-e4657283
        SecurityGroups:
            - !Ref SecurityGroup
        InstanceType: t2.micro
        BlockDeviceMappings:
            -
                DeviceName: "/dev/xvda"
                Ebs:
                    VolumeType: gp2
                    VolumeSize: 30
        UserData:
            Fn::Base64: !Sub |
                #!/bin/bash
                echo ECS_CLUSTER=${ECSClusterName} >> /etc/ecs/ecs.config
                sudo yum install -y aws-cfn-bootstrap
                sleep 60
                /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource AutoScalingGroup --region ${AWS::Region}
AutoScalingGroup:
    Type: "AWS::AutoScaling::AutoScalingGroup"
    Properties:
        LaunchConfigurationName: sample
        DesiredCapacity: 2
        MaxSize: 3
        MinSize: 2
        VPCZoneIdentifier:
            - !Ref PublicSubnet1
            - !Ref PublicSubnet2
    CreationPolicy:
        ResourceSignal:
            Count: 1
            Timeout: PT5M
    UpdatePolicy:
        AutoScalingReplacingUpdate:
            WillReplace: true

5.まとめ

もう少しきれいな書き方がありそうだけど、実運用でよくある要件の参考程度になれば幸いです。
EC2のASGまわりの設定は、従来のECSだとこのような形で大分インフラ側を意識しないといけない構成です。今後、re:Inventで発表されたEKSやFargateなどとも比べながら、本環境をアップデートしていければよいなと思います。

続きを読む

KubernetesでAWS ALBを自動作成する〜ついでにRoute53 Record Setも

kube-ingress-aws-controllerを使います。

kube-ingress-aws-controllerとは

Zalandoが公開している、Kubernetes用のIngress Controllerの一つです。

ZalandoはKubernetes界隈では著名な、ヨーロッパでファッションECをやっている企業です。
Kubernetesコミュニティへの様々な形で貢献していて、今回紹介するkube-aws-ingerss-controllerや先日紹介したexternal-dnsもその一つです。

何かできるのか

KubernetesユーザがAWSを全く触らずとも

  • ALBの自動作成
  • ALBに割り当てるTLS証明書(ACM管理)を自動選択

をしてくれます。

使い方

KubernetesのIngressリソースを普段通りつくります。

kubectl create -f myingress.yaml

すると、1~2分ほどでIngressリソースに書いたホスト名でインターネットからアクセスできるようになります。

open https://myingress.exapmle.com

Ingress Controllerとは

KubernetesのIngressはL7ロードバランサのスペックのようなもので、そのスペックから実際にL7ロードバランサをセットアップするのがIngress Controllerの役割です。

coreos/alb-ingress-controllerとの違い

coreos/alb-ingress-controller

  • Ingressリソース一つに対して、1 ALBをつくります

zalando-incubator/kube-ingress-aws-controller

  • ACM証明書のドメイン一つに対して、ALBを割り当てます
  • 同じドメイン名に対するルートを含むIngressリソースは、一つのALBにまとめられます
  • ALBのターゲットグループにEC2インスタンスを割り当てるところまでしかやってくれない!ので、実際にIngressとして利用するためには他のIngress Controllerを併用する必要があります

kube-ingress-aws-controllerのセットアップ手順

Security Groupの作成

kube-ingress-aws-controllerはALBに割り当てるSecurity Groupまでは自動作成してくれないので、AWSコンソール等で作成します。

kube-ingerss-aws-controllerのドキュメントにはCloudFormationを使った手順がかいてあります。

同等のSecurity Groupをawscliで作る場合は以下のようなコマンドを実行します。

CLUSTER_NAME=...
VPC_ID=vpc-...

aws ec2 create-security-group \
  --description ${CLUSTER_NAME}-kube-aws-ingress-controller-alb \
  --group-name ${CLUSTER_NAME}-kube-aws-ingress-controller-alb \
  --vpc-id $VPC_ID | tee sg.json

SG_ID=$(jq -r '.GroupId' sg.json)

aws ec2 create-tags --resources $SG_ID --tags \
  "Key=\"kubernetes.io/cluster/$CLUSTER_NAME\",Value=owned" \
  "Key=\"kubernetes:application\",Value=kube-ingress-aws-controller"

aws ec2 authorize-security-group-ingress \
  --group-id $SG_ID \
  --ip-permissions '[{"IpProtocol": "tcp", "FromPort": 80, "ToPort": 80, "IpRanges": [{"CidrIp": "0.0.0.0/0"}]}, {"IpProtocol": "tcp", "FromPort": 443, "ToPort": 443, "IpRanges": [{"CidrIp": "0.0.0.0/0"}]}]'

aws ec2 describe-security-groups --group-id $SG_ID

# あとで: 不要になったらクラスタやVPCの削除前に以下のように削除
aws ec2 delete-security-group --group-id $SG_ID

IAMポリシーの割当

kube-ingerss-aws-controllerのドキュメントにIAM Policy Statementの一覧がかいてありますが、要約すると

  • CloudFormationスタックのCRUD権限
  • ACM証明書、VPC、RouteTable、Subnet、Security Group、AutoScalingGroup、EC2 InstanceのGet/List/Describe権限
  • ALBのCRUD権限

が必要です。

kube-awsのcluster.yamlの場合は、以下のように書きます。

worker:
  nodePools:
  - iam:
      policy:
        statements:
        - effect: Allow
          actions:
          - "autoscaling:DescribeAutoScalingGroups"
          - "autoscaling:AttachLoadBalancers"
          - "autoscaling:DetachLoadBalancers"
          - "autoscaling:DetachLoadBalancerTargetGroup"
          - "autoscaling:AttachLoadBalancerTargetGroups"
          - "elasticloadbalancing:AddTags"
          - "elasticloadbalancing:DescribeLoadBalancers"
          - "elasticloadbalancing:CreateLoadBalancer"
          - "elasticloadbalancing:DeleteLoadBalancer"
          - "elasticloadbalancing:DescribeListeners"
          - "elasticloadbalancing:CreateListener"
          - "elasticloadbalancing:DeleteListener"
          - "elasticloadbalancing:DescribeTags"
          - "elasticloadbalancing:CreateTargetGroup"
          - "elasticloadbalancing:DeleteTargetGroup"
          - "elasticloadbalancing:DescribeTargetGroups"
          - "elasticloadbalancingv2:DescribeTargetGroups"
          - "elasticloadbalancingv2:DescribeLoadBalancers"
          - "elasticloadbalancingv2:CreateLoadBalancer"
          - "elasticloadbalancingv2:DeleteLoadBalancer"
          - "elasticloadbalancingv2:DescribeListeners"
          - "elasticloadbalancingv2:CreateListener"
          - "elasticloadbalancingv2:DeleteListener"
          - "elasticloadbalancingv2:DescribeTags"
          - "elasticloadbalancingv2:CreateTargetGroup"
          - "elasticloadbalancingv2:DeleteTargetGroup"
          - "ec2:DescribeInstances"
          - "ec2:DescribeSubnets"
          - "ec2:DescribeSecurityGroup"
          - "ec2:DescribeRouteTables"
          - "ec2:DescribeVpcs"
          - "acm:ListCertificates"
          - "acm:DescribeCertificate"
          - "iam:ListServerCertificates"
          - "iam:GetServerCertificate"
          - "cloudformation:Get*"
          - "cloudformation:Describe*"
          - "cloudformation:List*"
          - "cloudformation:Create*"
          - "cloudformation:Delete*"
          resources:
          - "*"

kube-aws-ingress-controllerをデプロイ

$ kubectl apply -f kube-aws-ingress-controller.yaml
kube-aws-ingress-controller.yaml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: kube-ingress-aws-controller
  namespace: kube-system
  labels:
    application: kube-ingress-aws-controller
    component: ingress
spec:
  replicas: 1
  selector:
    matchLabels:
      application: kube-ingress-aws-controller
      component: ingress
  template:
    metadata:
      labels:
        application: kube-ingress-aws-controller
        component: ingress
    spec:
      containers:
      - name: controller
        image: registry.opensource.zalan.do/teapot/kube-ingress-aws-controller:latest
        env:
        - name: AWS_REGION
          value: ap-northeast-1

併用するIngress Controllerをデプロイ

今回はskipperを使ってみます。

apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
  name: skipper-ingress
  namespace: kube-system
  labels:
    component: ingress
spec:
  selector:
    matchLabels:
      application: skipper-ingress
  updateStrategy:
    type: RollingUpdate
  template:
    metadata:
      name: skipper-ingress
      labels:
        component: ingress
        application: skipper-ingress
    spec:
      hostNetwork: true
      containers:
      - name: skipper-ingress
        image: registry.opensource.zalan.do/pathfinder/skipper:latest
        ports:
        - name: ingress-port
          containerPort: 9999
          hostPort: 9999
        args:
          - "skipper"
          - "-kubernetes"
          - "-kubernetes-in-cluster"
          - "-address=:9999"
          - "-proxy-preserve-host"
          - "-serve-host-metrics"
          - "-enable-ratelimits"
          - "-experimental-upgrade"
          - "-metrics-exp-decay-sample"
          - "-kubernetes-https-redirect=true"
        resources:
          limits:
            cpu: 200m
            memory: 200Mi
          requests:
            cpu: 25m
            memory: 25Mi
        readinessProbe:
          httpGet:
            path: /kube-system/healthz
            port: 9999
          initialDelaySeconds: 5
          timeoutSeconds: 5

WorkerノードのSecurity Group設定変更

今回はskipperをつかうことにしたので、kube-ingress-aws-controllerが作成したALBからアクセスする先はskipper(がいるEC2インスタンス)になります。

Security GroupへALBからskipperがいるEC2インスタンスへの通信をブロックしたままだとGateway Timeoutになってしまいます。そうならないように、ALB用につくったSGから、WorkerノードのSGへの9999番ポート(kube-ingress-aws-controllerと組み合わせて使うskipperのhostPortに指定した)の通信を許可しましょう。

ALB側SGのOutboundを絞っていないのであれば、Worker側SGのInboundを追加すればOKです。

Ingressリソースの作成

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: nginx
spec:
  rules:
  - host: nginx-ingress.example.com
    http:
      paths:
      - backend:
          serviceName: nginx
          servicePort: http

---

apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  type: ClusterIP
  ports:
  - name: http
    port: 80
    targetPort: 80
  selector:
    app: nginx

---

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: nginx
spec:
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx
        name: nginx
        ports:
        - containerPort: 80

ログの確認

$ k logs kube-ingress-aws-controller-6bbd8f9d6c-bcqph
2017/12/06 12:33:52 starting /bin/kube-ingress-aws-controller
2017/12/06 12:33:54 controller manifest:
2017/12/06 12:33:54     kubernetes API server:
2017/12/06 12:33:54     Cluster ID: k8s3
2017/12/06 12:33:54     vpc id: vpc-12345678
2017/12/06 12:33:54     instance id: i-07e29f841f676ca00
2017/12/06 12:33:54     auto scaling group name: k8s3-Nodepool1-MMF7MXKI9350-Workers-BZWB5IAV7JW8
2017/12/06 12:33:54     security group id: sg-8368a6fa
2017/12/06 12:33:54     private subnet ids: []
2017/12/06 12:33:54     public subnet ids: [subnet-12345678 subnet-23456789]
2017/12/06 12:33:54 Start polling sleep 30s

30秒経過後、以下のようにCloudFormationスタックが作成される。

2017/12/06 12:34:24 Found 1 ingresses
2017/12/06 12:34:24 Found 0 stacks
2017/12/06 12:34:24 Have 1 models
2017/12/06 12:34:24 creating stack for certificate "arn:aws:acm:ap-northeast-1:myawsaccountid:certificate/64f33935-05ac-4e6b-b1ae-58973d556a74" / ingress ["kube-system/nginx"]
2017/12/06 12:34:25 stack "arn:aws:cloudformation:ap-northeast-1:myawsaccountid:stack/k8s3-b9dbfe3/caf1f3a0-da81-11e7-9e21-500c28b97482" for certificate "arn:aws:acm:ap-northeast-1:myawsaccountid:certificate/64f33935-05ac-4e6b-b1ae-58973d556a74" created
2017/12/06 12:34:25 Start polling sleep 30s

作成されたAWSリソースの確認

1〜2分待ってスタックがCREATE_COMPLETE状態になれば成功。

コンソールでCloudFormationスタックのResourcesの内容を見ると、何がつくられたのかがわかる。

image.png

つくられるのは以下の4つ。

  • HTTPListener: 80番ポート用のListener
  • HTTPSListener: 443番ポート用のListener
  • LB: ALB
  • TG: kube-ingress-aws-controllerがデプロイされているノードが登録されたTargetGroup

ALB

リスナー

443番ポート用のListenerには、Ingressリソースに書いたドメインに対応するACM証明書が選択されています。

今回は*.example.com用のワイルドカード証明書を事前に用意しておいたのですが、Ingressにnginx-ingress.example.comというホスト名を設定したところ、ちゃんとワイルドカード証明書を探し出してくれました(かしこい)。

image.png

ターゲットグループ

kube-aws-ingress-controllerがデプロイされたノードのASGをコンソールでみてみると、ターゲットグループに割り当てられていました。EC2インスタンスを直接TargetGroupに登録していくような方法だとインスタンスが落ちた場合などが怖いですが、ちゃんとしてますね。

image.png

Route53 RecordSetの作成

これだけだとALBが作成されただけなので、nginx-ingress.example.comでアクセスできないはずです。

しかし、昨日デプロイしたexternal-dnsがIngressリソースとALBを検知して、勝手にRecordSetをつくってくれていました。

stern_external-dns.log
external-dns-768686fd4c-zpnlx external-dns time="2017-12-06T12:43:53Z" level=info msg="Desired change: CREATE nginx-ingress.example.com A"
external-dns-768686fd4c-zpnlx external-dns time="2017-12-06T12:43:53Z" level=info msg="Desired change: CREATE nginx-ingress.example.com TXT"
external-dns-768686fd4c-zpnlx external-dns time="2017-12-06T12:43:53Z" level=info msg="Record in zone example.com. were successfully updated"

image.png

ちゃんとALBへのA(lias)レコードを作成してくれていますね。

 インターネットからアクセスしてみる

nginx-ingress.example.comにブラウザからアクセスしてみて、以下のようなnginxのウェルカムページが表示されてば成功です。おつかれさまでした。

image.png

まとめ

kube-ingress-aws-controllerを使うと、Kubernetesユーザはkubectl createするだけでALBとRecordSetをよしなにセットアップしてくれます。
ALBの作成・管理やRoute53 RecordSetの作成のためにいちいちインフラエンジニアを呼び出したくない!というようなセルフサービス好きの会社さんでは特に役立つのではないでしょうか?!

トラブルシューティング

unable to get details for instance “i-0346d738155e965d8”

IAMポリシーが足りないときに出るエラーです。

$ k logs kube-ingress-aws-controller-7f7974ff58-6bvv8
2017/12/06 07:11:51 starting /bin/kube-ingress-aws-controller
2017/12/06 07:11:53 unable to get details for instance "i-0346d738155e965d8": NoCredentialProviders: no valid providers in chain. Deprecated.
    For verbose messaging see aws.Config.CredentialsChainVerboseErrors

required security group was not found

Security Groupがないか、またはSecurityGroupのタグが間違っているか、EC2インスタンスにkubernetes.io/cluster/クラスタ名=ownedというタグがついていない場合のエラーです。

$ k logs kube-ingress-aws-controller-7f7974ff58-xqgrq
2017/12/06 08:10:40 starting /bin/kube-ingress-aws-controller
2017/12/06 08:10:41 required security group was not found

CloudFormationで「At least two subnets in two different Availability Zones must be specified」

KubernetesのWorkerノードのASGが単一のAZに割り当てられているときのエラー。ALBの仕様で、最低2つのAZが必要。

kube-ingress-aws-controller-7f7974ff58-rp7t8 controller 2017/12/06 12:01:06 Start polling sleep 30s
kube-ingress-aws-controller-7f7974ff58-rp7t8 controller 2017/12/06 12:01:36 Found 1 ingresses
kube-ingress-aws-controller-7f7974ff58-rp7t8 controller 2017/12/06 12:01:37 Found 0 stacks
kube-ingress-aws-controller-7f7974ff58-rp7t8 controller 2017/12/06 12:01:37 Have 1 models
kube-ingress-aws-controller-7f7974ff58-rp7t8 controller 2017/12/06 12:01:37 creating stack for certificate "arn:aws:acm:ap-northeast-1:myaccountid:certificate/64f33935-05ac-4e6b-b1ae-58973d556a74" / ingress ["kube-system/nginx"]
kube-ingress-aws-controller-7f7974ff58-rp7t8 controller 2017/12/06 12:01:37 stack "arn:aws:cloudformation:ap-northeast-1:myaccountid:certificate:stack/k8s3-b9dbfe3/360e1830-da7d-11e7-99f7-500c596c228e" for certificate "arn:aws:acm:ap-northeast-1:myaccountid:certificate:certificate/64f33935-05ac-4e6b-b1ae-58973d556a74" created

image.png

instance is missing the “aws:autoscaling:groupName” tag

ASG以外で作ったEC2インスタンスにkube-ingress-aws-controllerがデプロイされてしまったときのエラー。

$ k logs kube-ingress-aws-controller-7f7974ff58-m6ss2
2017/12/06 12:25:59 starting /bin/kube-ingress-aws-controller
2017/12/06 12:25:59 instance is missing the "aws:autoscaling:groupName" tag

kube-aws-ingress-controllerは、デフォルトではASGに設定されたSubnetをALBのSubnetに流用する。
そのためにASGを探すとき、EC2インスタンスについたaws:autoscaling:groupNameというASGが自動的につけてくれるタグをヒントにするため、ASG以外でつくったEC2インスタンスではこのエラーが出てしまう。

Ref: Spot Fleet support · Issue #105 · zalando-incubator/kube-ingress-aws-controller

Issueも出ているが、まだASG以外は対応していない。ワークアラウンドとしては、kube-ingress-aws-controllerのaffinityでASGでつくったノードにだけスケジュールされるようにすることが考えられる。

kube-awsの場合、awsNodeLabels機能をオンにすると、ASGでつくったノードには”kube-aws.coreos.com/autoscalinggroup”というラベルが付与されるので、それを前提にすると以下のようなaffinityをかけばOK。

              affinity:
                nodeAffinity:
                  requiredDuringSchedulingIgnoredDuringExecution:
                    nodeSelectorTerms:
                    - matchExpressions:
                      - key: "kube-aws.coreos.com/autoscalinggroup"
                        operator: "Exists"

504 Gateway Time-out

ALB経由でnginxにアクセスしようとしてこのエラーがかえってきた場合、ALB用につくったセキュリティグループからkube-aws-ingress-controllerが動いているEC2インスタンスへのアクセスを許可できていない可能性があります。

EC2インスタンス側のSGに、ALB用SGからの9999番ポート(kube-ingress-aws-controllerと組み合わせて使うskipperのhostPortに指定した)への通信をを許可するようなInboundルールを追加しましょう。

続きを読む