[小ネタ]Amazon Managed Workflowを既存VPC上に構築するためのポイント

2021.03.25

どーもsutoです。

最近データ分析や機械学習のワークフローやバッチジョブのためにAirflowを勉強しています。また検証環境の構築に、AWSのマネージドサービスとしてリリースされたAmazon Managed Workflow for Apache Airflow(MWAA)を触っています。

MWAAの特徴や料金の概要については以下のブログがわかりやすいです。

しかし、構築した環境は時間で課金され、Webサーバ用インスタンスも一時停止できる機能はありません。(本来の運用では24時間稼働しておくものだから当然ですけどね) 料金体系も最小構成で「$0.49/hour」なので、環境を作っただけで放置しておくと決して安くはない費用が発生します。

毎日の業務で使用するわけではないので、検証で使用する時のみ環境を立ち上げ/削除を行っているのですが、手動でやるのはさすがに手間になるので、必要なリソースを自動構築するCloudFormationテンプレートを作成しました。

また、今回は既存VPCを利用して環境を構築してみようと思いますので、MWAA環境におけるVPCの要件や全体のAWS料金について抑えるべきポイントを含めてご紹介していきます。

MWAA環境のためのVPC要件と料金の話

公式ドキュメントによると、MWAA環境用のVPCには以下の要件があります。

  1. 同じリージョン内の2つの異なるアベイラビリティーゾーンにある2つのプライベートサブネット。
  2. また、次のいずれかが必要になります。
  • 上記プライベートサブネットがインターネットに接続するためのNAT Gatewayを配置した2つのパブリックサブネット。
  • 以下のVPCエンドポイント(AWS Private Link)が作成されていること
    • Amazon CloudWatch
    • CloudWatch Logs
    • Amazon ECR
    • Amazon S3
    • Amazon SQS
    • AWS Key Management Service

ここで、NAT GatewayもVPCエンドポイントも時間による課金が発生することにご注意ください。

つまり、MWAAの環境立ち上げた際のAWS料金は、

  • MWAA環境料金 + MWAAワーカー料金 + メタデータストレージ料金 + (NAT Gateway料金 or PrivateLink(6つ)料金)

となり、最小構成の概算見積でも月額で$500〜はかかることになりそうです。

MWAAのメリットと課題

(2021/3/24時点のものとなりますが)

メリット

  • マネージドサービスのため環境の維持が楽
  • Dag、Pligin、RequirementsはS3バケットで管理
  • airflow.cfgの設定がコンソールで直接管理できる
  • IAMの機能でログインユーザ管理ができる

課題と制約

  • スケジューラ、ワーカー、またはWebサーバーインスタンスにSSHで接続することはできない
    • 各種ログはCloudwatch Logsに出力されます
  • メタデータデータベースに直接アクセスしてDAG情報を照会することはできない。また、メタデータをエクスポートする機能はまだ実装されていない
    • DAG関連の情報はUIメニューからAPIを利用してアクセス可能
  • Airflowのバージョンが限定されている
    • AWSは以前のAirflowバージョンでセキュリティ上の懸念を認識していたため、MWAAは最新の安定バージョンのみをサポートしているとのこと
    • 2.0はまだ未対応
  • Webサーバインスタンスの一時停止はできない

その他によくある質問にもいろいろ記載されていますのでご参照ください。

既存VPC上にMWAA環境を構築してみた

では実際に既存VPCを利用してMWAA環境をCloudFormationで構築してみましょう。

※MWAA環境用VPCを新規で構築する場合は公式のテンプレートでVPCを自動構築できますのでご参照ください。

  • 以下のような、既存VPCがあることを前提とします
    • 異なるAZにあるパブリックサブネット2つと、プライベートサブネット2つ
    • インターネットゲートウェイがアタッチ済
    • 各ルートテーブルに1つのサブネットが関連付け
    • MWAA用のセキュリティグループは作成済(デフォルトのSGでも可)

ネットワーク要件で不足しているリソースを一緒に追加構築するので、今回はCloudFormationで以下のリソースを構築します。

  • MWAA環境
  • MWAAの実行IAMロール
  • NAT Gateway2台
  • EIP2つ(NAT Gatewayアタッチ用)

※今回のMWAAのUIへのアクセスは パブリック とします。

※※プライベートの場合は環境構築後、プロキシサーバ構成またはELB構成などの追加のネットワーク設定が必要ですが本記事では紹介しません(参考

テンプレート

AWSTemplateFormatVersion: 2010-09-09
Description: Resources creates an Amazon Managed Workflows for Apache Airflow (MWAA) environment
Parameters:
  MWAAName:
    Description: The version of Apache Airflow.
    Type: String
    Default: MyAirflowEnvironment
  Version:
    Description: The version of Apache Airflow.
    Type: String
    Default: 1.10.12
  BucketName:
    Description: The name of the S3 bucket that your environment accesses
    Type: String
    Default: bucket-name
  DagS3Path:
    Description: The relative path to the DAGs folder after your S3 bucket name.
    Type: String
    Default: /dags
  PluginS3Path:
    Description: The relative path to the plugins.zip file after your S3 bucket name.
    Type: String
    Default: /plugins.zip
  RequirementsS3Path:
    Description: The relative path to the requirements.txt file after your S3 bucket name.
    Type: String
    Default: /requirements.txt
  EnvClass:
    Description: The environment class name.
    Type: String
    Default: mw1.small
    AllowedValues:
      - mw1.small
      - mw1.medium
      - mw1.large
  Accessmode:
    Description: The environment class name.
    Type: String
    Default: PUBLIC_ONLY
    AllowedValues:
      - PUBLIC_ONLY
      - PRIVATE_ONLY
  MaxWorkerServer:
    Type: Number
    Default: 5
  MaintenanceWindow:
    Description: 'format is DAY:HH:MM.'
    Type: String
    Default: 'SUN:03:00'
  PublicSubnetId1:
    Type: String
  PublicSubnetId2:
    Type: String
  PrivateSubnetId1:
    Type: String
  PrivateSubnetId2:
    Type: String
  MWAASGId:
    Type: String
  PrivateRouteTableId1:
    Type: String
  PrivateRouteTableId2:
    Type: String

Resources:
  # MWAA Environment
  MWAA:
    Type: 'AWS::MWAA::Environment'
    Properties:
      AirflowVersion: !Ref Version
      SourceBucketArn: !Sub 'arn:aws:s3:::${BucketName}'
      DagS3Path: !Sub 's3://${BucketName}${DagS3Path}'
      PluginsS3Path: !Sub 's3://${BucketName}${PluginS3Path}'
      RequirementsS3Path: !Sub 's3://${BucketName}${RequirementsS3Path}'
      EnvironmentClass: !Ref EnvClass
      ExecutionRoleArn: !GetAtt MWAARole.Arn
      LoggingConfiguration:
        TaskLogs:
          Enabled: true
          LogLevel: INFO
        SchedulerLogs:
          Enabled: false
          LogLevel: WARNING
        WebserverLogs:
          Enabled: false
          LogLevel: WARNING
        WorkerLogs:
          Enabled: false
          LogLevel: WARNING
        DagProcessingLogs:
          Enabled: false
          LogLevel: WARNING
      MaxWorkers: !Ref MaxWorkerServer
      Name: !Ref MWAAName
      NetworkConfiguration:
        SecurityGroupIds:
          - !Ref MWAASGId
        SubnetIds:
          - !Ref PrivateSubnetId1
          - !Ref PrivateSubnetId2
      WebserverAccessMode: !Ref Accessmode
      WeeklyMaintenanceWindowStart: !Ref MaintenanceWindow

  # IAM Role
  MWAARole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: !Sub '${MWAAName}-MWAA-role'
      Path: /service-role/
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - airflow-env.amazonaws.com
                - airflow.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Policies:
        - PolicyName: !Sub 'MWAA-${MWAAName}-Execution-Policy'
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: 'airflow:PublishMetrics'
                Resource:
                  - !Sub "arn:aws:airflow:${AWS::Region}:${AWS::AccountId}:environment/${MWAAName}"
              - Effect: Deny
                Action: 's3:ListAllMyBuckets'
                Resource:
                  - !Sub 'arn:aws:s3:::${BucketName}'
                  - !Sub 'arn:aws:s3:::${BucketName}/*'
              - Effect: Allow
                Action:
                  - 's3:GetObject*'
                  - 's3:GetBucket*'
                  - 's3:List*'
                Resource:
                  - !Sub 'arn:aws:s3:::${BucketName}'
                  - !Sub 'arn:aws:s3:::${BucketName}/*'
              - Effect: Allow
                Action:
                  - 'logs:CreateLogStream'
                  - 'logs:CreateLogGroup'
                  - 'logs:PutLogEvents'
                  - 'logs:GetLogEvents'
                  - 'logs:PutLogEvents'
                  - 'logs:GetLogRecord'
                  - 'logs:GetLogGroupFields'
                  - 'logs:GetQueryResults'
                Resource:
                  - !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:airflow-${MWAAName}-*'
              - Effect: Allow
                Action: 'logs:DescribeLogGroups'
                Resource: '*'
              - Effect: Allow
                Action: 'cloudwatch:PutMetricData'
                Resource: '*'
              - Effect: Allow
                Action:
                  - 'sqs:ChangeMessageVisibility'
                  - 'sqs:DeleteMessage'
                  - 'sqs:GetQueueAttributes'
                  - 'sqs:GetQueueUrl'
                  - 'sqs:ReceiveMessage'
                  - 'sqs:SendMessage'
                Resource:
                  - !Sub 'arn:aws:logs:${AWS::Region}:*:airflow-celery-*'
              - Effect: Allow
                Action:
                  - 'kms:Decrypt'
                  - 'kms:DescribeKey'
                  - 'kms:GenerateDataKey*'
                  - 'kms:Encrypt'
                NotResource:
                  - !Sub 'arn:aws:kms:*:${AWS::AccountId}:key/*'
                Condition:
                  StringLike:
                    'kms:ViaService':
                      - sqs.ap-northeast-1.amazonaws.com

  # NAT Gateway, EIP
  DefaultPrivateRoute1:
    Type: 'AWS::EC2::Route'
    Properties:
      RouteTableId: !Ref PrivateRouteTableId1
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway1
  PrivateSubnet1RouteTableAssociation:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties:
      RouteTableId: !Ref PrivateRouteTableId1
      SubnetId: !Ref PrivateSubnetId1
  DefaultPrivateRoute2:
    Type: 'AWS::EC2::Route'
    Properties:
      RouteTableId: !Ref PrivateRouteTableId2
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway2
  PrivateSubnet2RouteTableAssociation:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties:
      RouteTableId: !Ref PrivateRouteTableId2
      SubnetId: !Ref PrivateSubnetId2
  NatGateway1EIP:
    Type: 'AWS::EC2::EIP'
    Properties:
      Domain: vpc
  NatGateway2EIP:
    Type: 'AWS::EC2::EIP'
    Properties:
      Domain: vpc
  NatGateway1:
    Type: 'AWS::EC2::NatGateway'
    Properties:
      AllocationId: !GetAtt NatGateway1EIP.AllocationId
      SubnetId: !Ref PublicSubnetId1
  NatGateway2:
    Type: 'AWS::EC2::NatGateway'
    Properties:
      AllocationId: !GetAtt NatGateway2EIP.AllocationId
      SubnetId: !Ref PublicSubnetId2

これで作成したまま放置すると課金が発生するリソースを一括で作成・削除ができますね。入力したパラメータはクイックリンクを生成しておけば楽だし、CDKで管理するようにしてもOKですね。

まとめ

今回は既存VPCにMWAA環境を構築するテンプレートと、MWAAの特徴、課題点、VPC要件についてのご紹介でした。

Airflow2.0の対応やメタデータのエクスポート機能辺りは今後のアップデートに期待したいところです。スモール構成でいくつか簡単なジョブを動かしてみましたが、パフォーマンスはちょっとモッサリな印象でした。(あくまで主観です)

ですが、GCPのCloud Composerと違い、ワーカーがAutoscalingするという利点も加えて、Airflowの環境維持に関する負担は非常に軽減されると思います。