EC2をCloudFormationとAnsibleで構成管理する

エキサイト株式会社のみーです。

最近はコンテナばかり触っていますが、要件によってはEC2の非コンテナ環境で構築しなければいけないこともあると思います。そういう時に出てくるのが、構成管理どうしよう、という悩み。

ヘルパースクリプトを活用する

AWSに触れ始めた頃、CFnで全てを完結させたいという謎のモチベーションがありました。CFnのヘルパースクリプトを使うことで、その目的は達せられます。

docs.aws.amazon.com

例えば、SESにリレーするようにPostfixを設定したEC2を構築するとなると、以下のような感じ。

Resources:
  EC2Instance:
    Type: AWS::EC2::Instance
    Metadata:
      AWS::CloudFormation::Init:
        config:
          files:
            /etc/cfn/cfn-hup.conf:
              content: !Sub |
                [main]
                stack=${AWS::StackId}
                region=${AWS::Region}
                interval=3
              mode: "000400"
              owner: root
              group: root
            /etc/cfn/hooks.d/cfn-auto-reloader.conf:
              content: !Sub |
                [cfn-auto-reloader-hook]
                triggers=post.update
                path=Resources.EC2Instance.Metadata.AWS::CloudFormation::Init
                action=/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource EC2Instance --region ${AWS::Region}
                runas=root
              mode: "000400"
              owner: root
              group: root
            /tmp/sasl_passwd:
              content: |
                [email-smtp.us-east-1.amazonaws.com]:587 foo:bar
              mode: "000600"
              owner: root
              group: root
          commands:
            01_postconf:
              command: |
                postconf -e \
                  "relayhost = [email-smtp.us-east-1.amazonaws.com]:587" \
                  "smtp_sasl_auth_enable = yes" \
                  "smtp_sasl_security_options = noanonymous" \
                  "smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd" \
                  "smtp_use_tls = yes" \
                  "smtp_tls_security_level = encrypt" \
                  "smtp_tls_note_starttls_offer = yes" \
                  "smtp_tls_CAfile = /etc/ssl/certs/ca-bundle.crt"
            02_cp_sasl_passwd:
              command: cp /tmp/sasl_passwd /etc/postfix/sasl_passwd
            03_postmap:
              command: postmap hash:/etc/postfix/sasl_passwd
            04_chown_db:
              command: chown root:root /etc/postfix/sasl_passwd.db
            05_chmod_db:
              command: chmod 600 /etc/postfix/sasl_passwd.db
          services:
            sysvinit:
              cfn-hup:
                enabled: true
                ensureRunning: true
                files:
                  - /etc/cfn/cfn-hup.conf
                  - /etc/cfn/hooks.d/cfn-auto-reloader.conf
              postfix:
                enabled: true
                ensureRunning: true
                files:
                  - /tmp/sasl_passwd
    Properties:
      ImageId: !Ref ImageId
      KeyName: !Ref KeyName
      SubnetId: !Ref SubnetId
      AvailabilityZone: !Ref SubnetAz
      InstanceType: !Ref InstanceType
      SecurityGroupIds:
        - !Ref SecurityGroupId
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash -xe
          yum update -y aws-cfn-bootstrap
          /opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource EC2Instance --region ${AWS::Region}
          /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource EC2Instance --region ${AWS::Region}

設定ファイルやコマンドなどをテンプレート内に記述するわけですが・・・見ての通り、決して美しいとは言えません。もちろん最適化の余地はあるのですが、このまま使い続ける気にはなりません。本来の目的は構成管理であって、テンプレートを美しく記述することではないわけです。

ということで、別の方法を考えることに。

Ansibleを使う

AWSの構成管理サービスといえばAWS OpsWorksですね。PuppetやChefを使い慣れているのであればベターな選択かと思います。

aws.amazon.com

なのですが、エキサイトでは構成管理ツールにAnsibleを採用することが多かったりします。個人的にもAnsibleは使いやすくて好きなので、今回はOpsWorksは使いません。

AWSでAnsibleプレイブックを実行する場合、SSMドキュメントの AWS-ApplyAnsiblePlaybooks を利用するのがおすすめです。
事前にS3にプレイブックをアップロードしておき、あとはマネコンなどからポチるだけで実行されます。とても快適。

docs.aws.amazon.com

以下のサンプルでは、ansible-playbook-${AWS::Region}-${AWS::AccountId} というS3バケットを用意しておき、playbook.zip という名前で対象のプレイブックをアップロードしていることが前提になっています。

Resources:
  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref ImageId
      KeyName: !Ref KeyName
      SubnetId: !Ref SubnetId
      AvailabilityZone: !Ref SubnetAz
      InstanceType: !Ref InstanceType
      SecurityGroupIds:
        - !Ref SecurityGroupId

  ApplyAnsiblePlaybooks:
    Type: AWS::SSM::Association
    Properties:
      Name: AWS-ApplyAnsiblePlaybooks
      AssociationName: sample-association
      WaitForSuccessTimeoutSeconds: 300
      Targets:
        - Key: InstanceIds
          Values:
            - !Ref EC2Instance
      OutputLocation:
        S3Location:
          OutputS3BucketName: !Sub ansible-playbook-${AWS::Region}-${AWS::AccountId}
          OutputS3KeyPrefix: log
      Parameters:
        SourceType:
          - "S3"
        SourceInfo:
          - !Sub |
            {"path": "https://ansible-playbook-${AWS::Region}-${AWS::AccountId}.s3-${AWS::Region}.amazonaws.com/playbook.zip"}
        InstallDependencies:
          - "True"
        PlaybookFile:
          - "playbook.yml"
        ExtraVariables:
          - "SSM=True"
        Check:
          - "False"
        Verbose:
          - "-v"

これをデプロイすれば、良い感じにEC2インスタンスが作成されて、良い感じにAnsibleプレイブックが実行されます。プレイブックを変更した場合は、SSMのステートマネージャーから即座に適用できます。いいね。

トラブルシューティング

上記のテンプレート、EC2を閉域網に構築した場合には失敗します。InstallDependencies パラメータをTrueにすると、Ansibleや依存関係にあるPythonなどのミドルウェアをインストールしてくれるのですが、Amazon Linux 2の場合は以下のようなコマンドが実行されます。

sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
sudo yum install -y ansible

思いっきり外に出ようとしているな。

が、そういうことであれば InstallDependencies パラメータはFalseにしておき、その前に AWS-RunShellScript でExtras LibraryからAnsibleをインストールしてしまえば良さそう。

  InstallAnsible:
    Type: AWS::SSM::Association
    Properties:
      Name: AWS-RunShellScript
      AssociationName: sample-association
      WaitForSuccessTimeoutSeconds: 300
      Targets:
        - Key: InstanceIds
          Values:
            - !Ref EC2Instance
      OutputLocation:
        S3Location:
          OutputS3BucketName: !Sub ansible-playbook-${AWS::Region}-${AWS::AccountId}
          OutputS3KeyPrefix: log
      Parameters:
        commands:
          - sudo amazon-linux-extras install ansible2 -y

S3のVPCエンドポイントを設定しておく必要はありますが、これなら閉域網でも問題ありませんね。

おわりに

CFnでは、AWSリソースの構成管理だけに徹するのが良いのかなと思います。銀の弾丸は存在しませんので、最適なツールを選択していきたいところです。