Ubuntu 20.04 LTSにCloudWatch エージェントをインストールして設定ファイルを作成するCFnを作った話

## はじめに


CloudFormationのヘルパースクリプト、とっても便利ですよね。テンプレートにスクリプトを定義しておくだけでEC2を作成するときにソフトウェアをインストールしたりサービスを開始したりできちゃいます。

今回は、そのヘルパースクリプトを使ってCloudwatch agentのインストールと設定ファイルの作成を行う機会があったので忘れないように書いていきたいと思います。


### 構築する環境
- Ubuntu Server 20.04 LTS (HVM), SSD Volume Type（ami-0f2dd5fc989207c82）


## CFnをつくる
### ヘルパースクリプトの準備
さっそく、ヘルパースクリプトを使ってcloudwatch agentをインストール！
といきたいところですが、残念ながらUbuntuにはデフォルトでインストールされていません。なので、まずはaws-cfn-bootstrapパッケージをダウンロードしていきます。EC2が起動するときに実行されるUserDateにヘルパースクリプトに下記のスクリプトを追加します。

```bash

apt-get update
apt-get install -y python-setuptools
mkdir -p /opt/aws/bin
apt-get install -y wget
wget https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz
python3 -m easy_install --script-dir /opt/aws/bin aws-cfn-bootstrap-py3-latest.tar.gz
```
[▶ ヘルパースクリプトリファレンス](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-helper-scripts-reference.html) 

### CloudWatchエージェントのインストール

次に、cloudwatchエージェントをインストールします。これも同様にUserDateに追加します。

```bash

wget https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb -O /tmp/amazon-cloudwatch-agent.deb
dpkg -i /tmp/amazon-cloudwatch-agent.deb
/opt/aws/bin/cfn-init -v --stack ${AWS::StackId} --resource EC2 --region ${AWS::Region} --configsets default
/opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackId} --resource EC2 --region ${AWS::Region}
```

### amazon-cloudwatch-agent.jsonの作成
ヘルパースクリプトとcloudwatchエージェントのインストールまでできたので、次は設定ファイルの`amazon-cloudwatch-agent.json`を作成していきましょう。設定ファイルの書き方は[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-Configuration-File-Details.html) を参考にしました。


### ヘルパースクリプトの作成
次はようやくヘルパースクリプトです。
`AWS::CloudFormation::Init`タイプを使うことでcfn-init ヘルパースクリプト用のメタデータを EC2に取り込むことができます。


- cfn-hup
リソースのメタデータの変更を検出して変更された時にユーザーが指定した操作を実行してくれるデーモンさんです。今回はトリガーとしてスタックの更新イベントを検知していて、検知するとhooks.d配下の設定ファイルにあるアクションが実行されます。

- hooks.conf 設定ファイル
cfn-hupデーモンが定期的に呼び出すユーザーアクションが定義されてます。

- cfn-hup.conf 設定ファイル
cfn-hupデーモンのターゲットとなるスタックの名前とAWS認証情報が格納されてます。

今回は、スタックを新規で作成するときは`default `に、スタックの更新でメタデータを更新するときには`UpdateEnvironment`の方に行くようにしています。


``` yaml
  EC2:
    Type: AWS::EC2::Instance
    Metadata:
      AWS::CloudFormation::Init:
        configSets:
          default:
            - "01_setupCfnHup"
            - "02_config_amazon-cloudwatch-agent"
            - "03_restart_amazon-cloudwatch-agent"
          UpdateEnvironment:
            - "02_config_amazon-cloudwatch-agent"
            - "03_restart_amazon-cloudwatch-agent"
 
        01_setupCfnHup:
          files:
            /etc/cfn/cfn-hup.conf:
              content: !Sub |
                [main]
                stack=${AWS::StackName}
                region=${AWS::Region}
                interval=1
              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.EC2.Metadata.AWS::CloudFormation::Init
                action=/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource EC2 --configsets UpdateEnvironment --region ${AWS::Region}
                runas=root
              mode: '000400'
              owner: root
              group: root
            /lib/systemd/system/cfn-hup.service:
              content: !Sub |
                [Unit]
                Description=cfn-hup daemon
                [Service]
                Type=simple
                ExecStart=/opt/aws/bin/cfn-hup
                Restart=always
                [Install]
                WantedBy=multi-user.target
          commands:
            01enable_cfn_hup:
              command: "systemctl enable cfn-hup.service"
            02start_cfn_hup:
              command: "systemctl start cfn-hup.service"
        02_config_amazon-cloudwatch-agent:
          files:
            /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json:
              content: !Sub |
                {
                  // ここに　amazon-cloudwatch-agent.json　を書く//
                }
              mode: '000644'
              owner: root
              group: root

        03_restart_amazon-cloudwatch-agent:
          commands:
            01_stop_service:
              command: "/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a stop"
            02_start_service:
              command: "/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -s -c file:/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json"
```

<br>

ちょっとつまったところなのですが、「03_restart_amazon-cloudwatch-agent」のコマンド 

```
/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -s -c file:/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json
```
ではjsonファイルをtoml形式の設定ファイル`amazon-cloudwatch-agent.toml`に変換して、エージェントを開始させているようです。なのでこのCFnで作成したEC2には、`amazon-cloudwatch-agent.json`というファイルは存在しません。

### CloudWatchエージェントのためのロールの作成
AWS リソースにアクセスするにはアクセス権限が必要です。EC2に必要な権限を与えたIAMロールを作成していきましょう。
以下のポリシーをアタッチしたロール`CloudwatchRole`を作成しました。

- AWSマネージドポリシー
    - CloudWatchAgentServerPolicy
    - CloudWatchAgentAdminPolicy
    - AmazonSSMManagedInstanceCore
- カスタムポリシー
    - 今回はS3とlogsにアクセス許可をするポリシーをつけました。状況に合わせてよきように。

``` yaml
  CloudwatchRole:
    Type: AWS::IAM::Role
    Properties: 
      RoleName: "CloudwatchRole"
      Path: /
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: /
      Policies:
        - PolicyName: CloudwatchRolePolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement: 
                  - Effect: Allow
                    Action:
                      // 省略 //
                    Resource: '*'
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
        - arn:aws:iam::aws:policy/CloudWatchAgentAdminPolicy
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore

```

### インスタンスプロファイルの作成
最後に、先ほど作成したロールをEC2に渡すためにインスタンスプロファイルを作成します。

``` yaml
  CWAgentInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: "/"
      Roles:
      - !Ref CloudwatchRole
```

## おわりに
CloudFormationを作成するのにいろいろとはまりましたが、CFnでぱぱっとできるとウィザードでポチポチ設定していくよりも簡単ですね！


まだまだ分からないことだらけのCloudFormation。奥が深い。


（他にもっといい方法があるよ、ここ間違えてるよ、というところがありましたら教えていただけると嬉しいです！）

## 参考サイト
- [CloudFormation ヘルパースクリプトリファレンス - AWS CloudFormation](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-helper-scripts-reference.html)

- [CloudWatch エージェント設定ファイルを手動で作成または編集する - Amazon CloudWatch](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-Configuration-File-Details.html)


- [Ubuntu または RHEL AMI に AWS CloudFormation ヘルパースクリプトをインストールする](https://aws.amazon.com/jp/premiumsupport/knowledge-center/install-cloudformation-scripts/)

- [cfn-hup - AWS CloudFormation](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-hup.html)

- [はじめてのCloudWatch Agent導入 (SSM & CentOS) - サーバーワークスエンジニアブログ](https://blog.serverworks.co.jp/tech/2020/01/28/cloudwtach-agent-ssm-centos/)


- [（初心者向け）EC2に CloudWatch エージェントをインストールして SSM で起動する | Developers.IO](https://dev.classmethod.jp/articles/ec2-how-to-install-cloudwatch-agent-for-beginner/)


- [CloudWatch Agent を試す - ngyukiの日記](https://ngyuki.hatenablog.com/entry/2019/01/30/173822)