CloudWatch エージェントの設定方法

はじめに

AWS EC2 では CPU 使用率や Disk I/O 、Network I/O 等のデータをデフォルトのメトリクスで取得できますが、 メモリ使用量やディスク使用率のデータは取れません。 これらはカスタムメトリクスとして登録することで CloudWatch から取得できるようになります。

カスタムメトリクスは PutMetricData API で登録できます。 データを収集・整形しこの API 呼び出しを定期実行させればメモリ使用量やディスク使用率を CloudWatch から取得できるようになります。 例えば以下のようなことをします。

  1. /proc/meminfodf からデータを取得し整形
  2. aws cli を実行するシェルスクリプトを実装
  3. cron で定期実行または while, sleep で時間間隔を設けて実行

取得するデータが少ない場合はこれでも運用できますが、データが多くなってくるとメンテナンスがし辛くなるでしょう。 このような場合のために 公式ドキュメント では CloudWatch エージェントと CloudWatch モニタリングスクリプトの2通りの方法が紹介されています。

CloudWatch エージェントはインスタンスに常駐してメモリ使用量やディスク使用率等のリソースデータを収集しカスタムメトリクスとして登録します。 CloudWatch モニタリングスクリプトPerl で実装されたスクリプトで、これを cron で定期実行させデータの取得とカスタムメトリクスの登録をします。 2020/05/14 時点では CloudWatch エージェントを用いたカスタムメトリクス生成方法が推奨されています。 加えて、CloudWatch モニタリングスクリプトより CloudWatch エージェントの方がプロセスごとの CPU、メモリ使用量等の細かいリソースデータが取得できます。 特に理由がなければ CloudWatch エージェントを使った方がよいでしょう。

ここでは実際に CloudWatch エージェントを使って EC2 インスタンスのリソースデータをカスタムメトリクスとして登録し、 CloudWatch Metrics で確認するまでの手順・設定方法を紹介します。

検証用リソース作成

検証用の EC2 インスタンスやそれに割り当てるための IAM Role, Instance Profile 等の AWS リソースを作成します。 CloudWatch エージェントを使用してカスタムメトリクスを登録するには CloudWatchAgentServerPolicy ポリシーが必要になります。 ここではそれに加えて Systems Manager Session Manager で EC2 にログインするために AmazonSSMManagedInstanceCore ポリシーも割り当てます。

terraform configuration 例

terraform {
  required_providers {
    aws = "~> 2.61"
  }
}

resource "aws_iam_instance_profile" "this" {
  role = aws_iam_role.this.name
}

resource "aws_iam_role" "this" {
  assume_role_policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "sts:AssumeRole",
            "Principal": {
               "Service": "ec2.amazonaws.com"
            },
            "Effect": "Allow"
        }
    ]
}
EOF
}

resource "aws_iam_role_policy_attachment" "AmazonSSMManagedInstanceCore" {
  role       = aws_iam_role.this.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_role_policy_attachment" "CloudWatchAgentServerPolicy" {
  role       = aws_iam_role.this.name
  policy_arn = "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy"
}

data "aws_ami" "amzn2" {
  most_recent = true

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-2.0.????????.?-x86_64-gp2"]
  }

  filter {
    name   = "state"
    values = ["available"]
  }

  owners = ["amazon"]
}

resource "aws_spot_instance_request" "this" {
  ami           = data.aws_ami.amzn2.id
  instance_type = "t3.micro"

  iam_instance_profile = aws_iam_instance_profile.this.id

  credit_specification {
    cpu_credits = "standard"
  }

  spot_price           = "0.01"
  spot_type            = "one-time"
  wait_for_fulfillment = true
}

output "instance_id" {
  value = aws_spot_instance_request.this.spot_instance_id
}

CloudWatch エージェント

CloudWatch エージェントはこれらのことができます。

  • リソースデータを収集しカスタムメトリクスとして登録
  • ログファイルを読み取り CloudWatch Logs へ送信

以下では CloudWatch エージェントのインストールや設定ファイルの書き方、反映方法等を説明します。

インストール

上記で作成した検証用 EC2 インスタンスに CloudWatch エージェントをインストールします。 まずは Systems Manager Session Manager でログインします。

$ aws ssm start-session --target i-028f726cdabfa5c14

Starting session with SessionId: 2020-05-09-213857-0ef6fb6e30fcf5cf5
sh-4.2$ sudo su - ec2-user

Download and Configure the CloudWatch Agent Using the Command Line - Amazon CloudWatch に記載されている CloudWatch エージェントのパッケージ先をディストリビューションに合わせて選択します。 ここでは Amazon Linux 2 用の rpm パッケージからインストールします。

$ sudo yum install -y -q -e 0 https://s3.amazonaws.com/amazoncloudwatch-agent/amazon_linux/amd64/latest/amazon-cloudwatch-agent.rpm
create group cwagent, result: 0
create user cwagent, result: 0

CloudWatch エージェントは /opt/aws/amazon-cloudwatch-agent にインストールされます。 ここに実行ファイルや設定ファイル、CloudWatch エージェントの実行ログが格納されます。

CloudWatch エージェントの設定をするには、amazon-cloudwatch-agent-ctl コマンドを使います。 このコマンドの詳細は amazon-cloudwatch-agent-ctl -h で確認することができます。

amazon-cloudwatch-agent-ctl -h の出力例

usage: amazon-cloudwatch-agent-ctl -a stop|start|status|fetch-config|append-config|remove-config [-m ec2|onPremise|auto] [-c default|ssm:<parameter-store-name>|file:<file-path>] [-s]

e.g.
1. apply a SSM parameter store config on EC2 instance and restart the agent afterwards:
    amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -c ssm:AmazonCloudWatch-Config.json -s
2. append a local json config file on onPremise host and restart the agent afterwards:
    amazon-cloudwatch-agent-ctl -a append-config -m onPremise -c file:/tmp/config.json -s
3. query agent status:
    amazon-cloudwatch-agent-ctl -a status

-a: action
    stop:                                   stop the agent process.
    start:                                  start the agent process.
    status:                                 get the status of the agent process.
    fetch-config:                           use this json config as the agent's only configuration.
    append-config:                          append json config with the existing json configs if any.
    remove-config:                          remove json config based on the location (ssm parameter store name, file name)

-m: mode
    ec2:                                    indicate this is on ec2 host.
    onPremise:                              indicate this is on onPremise host.
    auto:                                   use ec2 metadata to determine the environment, may not be accurate if ec2 metadata is not available for some reason on EC2.

-c: configuration
    default:                                default configuration for quick trial.
    ssm:<parameter-store-name>:             ssm parameter store name
    file:<file-path>:                       file path on the host

-s: optionally restart after configuring the agent configuration
    this parameter is used for 'fetch-config', 'append-config', 'remove-config' action only.

起動

インストール直後はまだ CloudWatch エージェントが起動されていません。

$ amazon-cloudwatch-agent-ctl -a status
{
  "status": "stopped",
  "starttime": "",
  "version": "1.237768.0"
}

設定ファイルを用意せずにエージェントを起動すると、AWS で用意されたデフォルト設定が反映され CloudWatch エージェントが起動されます。

$ sudo amazon-cloudwatch-agent-ctl -a start
amazon-cloudwatch-agent is not configured. Applying default configuration before starting it.
/opt/aws/amazon-cloudwatch-agent/bin/config-downloader --output-dir /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d --download-source default --mode ec2 --config /opt/aws/amazon-cloudwatch-agent/etc/common-config.toml --multi-config default
Successfully fetched the config and saved in /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d/default.tmp
Start configuration validation...
/opt/aws/amazon-cloudwatch-agent/bin/config-translator --input /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json --input-dir /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d --output /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.toml --mode ec2 --config /opt/aws/amazon-cloudwatch-agent/etc/common-config.toml --multi-config default
2020/05/10 00:18:42 Reading json config file path: /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d/default.tmp ...
Valid Json input schema.
I! Detecting runasuser...
No csm configuration found.
No log configuration found.
Configuration validation first phase succeeded
/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent -schematest -config /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.toml
Configuration validation second phase succeeded
Configuration validation succeeded
Created symlink from /etc/systemd/system/multi-user.target.wants/amazon-cloudwatch-agent.service to /etc/systemd/system/amazon-cloudwatch-agent.service.
Redirecting to /bin/systemctl restart amazon-cloudwatch-agent.service

2行目で amazon-cloudwatch-agent is not configured. Applying default configuration before starting it. と出力されていることから、デフォルト設定が反映されていることがわかります。

CloudWatch エージェントの実行ログはデフォルトでは /opt/aws/amazon-cloudwatch-agent/logs に格納されます。 ログからも起動できていることがわかります。

$ tail -f /opt/aws/amazon-cloudwatch-agent/logs/amazon-cloudwatch-agent.log
2020/05/10 05:18:10 I! Detected runAsUser: cwagent
2020/05/10 05:18:10 I! Change ownership to cwagent:cwagent
2020/05/10 05:18:10 I! Set HOME: /home/cwagent
2020-05-10T05:18:10Z I! cloudwatch: get unique roll up list []
2020-05-10T05:18:10Z I! cloudwatch: publish with ForceFlushInterval: 1m0s, Publish Jitter: 37s
2020-05-10T05:18:10Z I! Starting AmazonCloudWatchAgent (version 1.237768.0)
2020-05-10T05:18:10Z I! Loaded outputs: cloudwatch
2020-05-10T05:18:10Z I! Loaded inputs: disk mem
2020-05-10T05:18:10Z I! Tags enabled: host=ip-172-31-7-225.ap-northeast-1.compute.internal
2020-05-10T05:18:10Z I! Agent Config: Interval:1m0s, Quiet:false, Hostname:"ip-172-31-7-225.ap-northeast-1.compute.internal", Flush Interval:1s

デフォルト設定ファイル

デフォルト設定はどのようになっているのでしょうか。

設定ファイルは etc/ に保存されています。

$ ls -Rl /opt/aws/amazon-cloudwatch-agent/etc/
/opt/aws/amazon-cloudwatch-agent/etc/:
total 8
drwxr-xr-x 2 cwagent cwagent   21 May 10 05:18 amazon-cloudwatch-agent.d
-rw-rw-r-- 1 cwagent cwagent 1098 May 10 05:18 amazon-cloudwatch-agent.toml
-rw-r--r-- 1 cwagent cwagent  925 Jan 22 17:04 common-config.toml

/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d:
total 4
-rwxr-xr-x 1 cwagent cwagent 462 May 10 05:18 default

/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d/default がデフォルトの設定です。 拡張子がありませんが json ファイルです。

{
  "agent": {
    "run_as_user": "cwagent"
  },
  "metrics": {
    "metrics_collected": {
      "mem": {
        "measurement": ["mem_used_percent"]
      },
      "disk": {
        "measurement": ["used_percent"],
        "resources": ["*"]
      }
    },
    "append_dimensions": {
      "ImageId": "${aws:ImageId}",
      "InstanceId": "${aws:InstanceId}",
      "InstanceType": "${aws:InstanceType}",
      "AutoScalingGroupName": "${aws:AutoScalingGroupName}"
    }
  }
}

3行目の run_as_user で CloudWatch エージェントの実行ユーザを cwagent と設定しています。 6行目の metrics_collected セクションで収集するデータをメトリクスで指定されます。 8行目の mem_used_percent でメモリ使用率、11行目の used_percent でブロックデバイスごとのディスクスペース利用率を指定しています。 12行目の "resources": ["*"] では全ブロックデバイスを収集対象としており、/, /dev, /run 等のブロックデバイスが収集されます。

この時点で収集されたデータを CloudWatch Metrics で確認できます。

f:id:maya2250:20201109200430p:plain

toml 設定ファイル

etc/ ディレクトリ内には json ファイルの他に toml ファイルもあります。 設定ファイル周りの挙動を見る限り、以下のようにして CloudWatch エージェントに設定ファイルが反映されるのだと思われます。

  1. amazon-cloudwatch-agent-ctl コマンドで json から toml に変換
  2. CloudWatch エージェントに toml を読み込ませる

設定ファイルはローカルにある json ファイルまたは Systems Manager Parameter Store から toml に変換できます。 最初は json を直に読み込んでいるだろうと思っていたので、なぜ toml があるのか、わざわざ json から toml に変換する必要があるのかが疑問でした。 恐らくですが CloudWatch エージェントが influxdata/telegraf を元に実装されているからだと思います。 THIRD-PARTY-LICENSES ファイルから telegraf が使われていることがわかります。

$ grep telegraf THIRD-PARTY-LICENSES
** influxdata/telegraf; version 1.3 -- https://github.com/influxdata/telegraf

加えて telegraf の設定ファイルも toml 形式であることと設定ファイルの構成(inputs.cpu とか)が似ています。 公式ドキュメントを読む限り toml についてあまり言及されていないので、基本的には設定ファイルを json で書き amazon-cloudwatch-agent-ctl コマンドで反映するという使い方がよいでしょう。 もしかすると toml を直に編集して reload すれば telegraf 特有の機能を使えるかもしれませんが、CloudWatch エージェントとは別件になるのでここでは触れません。

ウィザード

設定ファイルはウィザードを用いて作成できます。 CloudWatch エージェントに付属している amazon-cloudwatch-agent-config-wizard を実行するとウィザードが始まり、15近くの質問に答えていくだけで簡単に設定ファイルを作成できます。 初回起動時や初めて触る場合はウィザードを使うのがよいでしょう。

ウィザード実行例

$ sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-config-wizard
=============================================================
= Welcome to the AWS CloudWatch Agent Configuration Manager =
=============================================================
On which OS are you planning to use the agent?
1. linux
2. windows
default choice: [1]:
1
Trying to fetch the default region based on ec2 metadata...
Are you using EC2 or On-Premises hosts?
1. EC2
2. On-Premises
default choice: [1]:
1
Which user are you planning to run the agent?
1. root
2. cwagent
3. others
default choice: [1]:
1
Do you want to turn on StatsD daemon?
1. yes
2. no
default choice: [1]:
2
Do you want to monitor metrics from CollectD?
1. yes
2. no
default choice: [1]:
2
Do you want to monitor any host metrics? e.g. CPU, memory, etc.
1. yes
2. no
default choice: [1]:
1
Do you want to monitor cpu metrics per core? Additional CloudWatch charges may apply.
1. yes
2. no
default choice: [1]:
1
Do you want to add ec2 dimensions (ImageId, InstanceId, InstanceType, AutoScalingGroupName) into all of your metrics if the info is available?
1. yes
2. no
default choice: [1]:
1
Would you like to collect your metrics at high resolution (sub-minute resolution)? This enables sub-minute resolution for all metrics, but you can customize for specific metrics in the output json file.
1. 1s
2. 10s
3. 30s
4. 60s
default choice: [4]:
2
Which default metrics config do you want?
1. Basic
2. Standard
3. Advanced
4. None
default choice: [1]:
3
Current config as follows:
{
        "agent": {
                "metrics_collection_interval": 10,
                "run_as_user": "root"
        },
        "metrics": {
                "append_dimensions": {
                        "AutoScalingGroupName": "${aws:AutoScalingGroupName}",
                        "ImageId": "${aws:ImageId}",
                        "InstanceId": "${aws:InstanceId}",
                        "InstanceType": "${aws:InstanceType}"
                },
                "metrics_collected": {
                        "cpu": {
                                "measurement": [
                                        "cpu_usage_idle",
                                        "cpu_usage_iowait",
                                        "cpu_usage_user",
                                        "cpu_usage_system"
                                ],
                                "metrics_collection_interval": 10,
                                "resources": [
                                        "*"
                                ],
                                "totalcpu": false
                        },
                        "disk": {
                                "measurement": [
                                        "used_percent",
                                        "inodes_free"
                                ],
                                "metrics_collection_interval": 10,
                                "resources": [
                                        "*"
                                ]
                        },
                        "diskio": {
                                "measurement": [
                                        "io_time",
                                        "write_bytes",
                                        "read_bytes",
                                        "writes",
                                        "reads"
                                ],
                                "metrics_collection_interval": 10,
                                "resources": [
                                        "*"
                                ]
                        },
                        "mem": {
                                "measurement": [
                                        "mem_used_percent"
                                ],
                                "metrics_collection_interval": 10
                        },
                        "netstat": {
                                "measurement": [
                                        "tcp_established",
                                        "tcp_time_wait"
                                ],
                                "metrics_collection_interval": 10
                        },
                        "swap": {
                                "measurement": [
                                        "swap_used_percent"
                                ],
                                "metrics_collection_interval": 10
                        }
                }
        }
}
Are you satisfied with the above config? Note: it can be manually customized after the wizard completes to add additional items.
1. yes
2. no
default choice: [1]:
1
Do you have any existing CloudWatch Log Agent (http://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AgentReference.html) configuration file to import for migration?
1. yes
2. no
default choice: [2]:
2
Do you want to monitor any log files?
1. yes
2. no
default choice: [1]:
2
Saved config file to /opt/aws/amazon-cloudwatch-agent/bin/config.json successfully.
Current config as follows:
{
        "agent": {
                "metrics_collection_interval": 10,
                "run_as_user": "root"
        },
        "metrics": {
                "append_dimensions": {
                        "AutoScalingGroupName": "${aws:AutoScalingGroupName}",
                        "ImageId": "${aws:ImageId}",
                        "InstanceId": "${aws:InstanceId}",
                        "InstanceType": "${aws:InstanceType}"
                },
                "metrics_collected": {
                        "cpu": {
                                "measurement": [
                                        "cpu_usage_idle",
                                        "cpu_usage_iowait",
                                        "cpu_usage_user",
                                        "cpu_usage_system"
                                ],
                                "metrics_collection_interval": 10,
                                "resources": [
                                        "*"
                                ],
                                "totalcpu": false
                        },
                        "disk": {
                                "measurement": [
                                        "used_percent",
                                        "inodes_free"
                                ],
                                "metrics_collection_interval": 10,
                                "resources": [
                                        "*"
                                ]
                        },
                        "diskio": {
                                "measurement": [
                                        "io_time",
                                        "write_bytes",
                                        "read_bytes",
                                        "writes",
                                        "reads"
                                ],
                                "metrics_collection_interval": 10,
                                "resources": [
                                        "*"
                                ]
                        },
                        "mem": {
                                "measurement": [
                                        "mem_used_percent"
                                ],
                                "metrics_collection_interval": 10
                        },
                        "netstat": {
                                "measurement": [
                                        "tcp_established",
                                        "tcp_time_wait"
                                ],
                                "metrics_collection_interval": 10
                        },
                        "swap": {
                                "measurement": [
                                        "swap_used_percent"
                                ],
                                "metrics_collection_interval": 10
                        }
                }
        }
}
Please check the above content of the config.
The config file is also located at /opt/aws/amazon-cloudwatch-agent/bin/config.json.
Edit it manually if needed.
Do you want to store the config in the SSM parameter store?
1. yes
2. no
default choice: [1]:
2
Program exits now.

設定ファイルの構文

よりカスタマイズをしたい場合や procstat, StatsD, collectd プラグインを使いたいのであれば設定ファイルを自分で作成する必要があります。 書き方の詳細は 公式ドキュメント を参考にするのがよいでしょう。 ここでは重要な部分だけ説明します。

設定ファイルは agentmetricslogs の3セクションからなります。

agent セクション

{
  "agent": {
    "metrics_collection_interval": 10,
    "run_as_user": "root",
    "debug": true
  },
...

CloudWatch エージェント自体の設定をします。 このセクションを省略した場合は、デフォルト値が使用されます。

  • metrics_collection_interval: 秒数指定で 1, 5, 10, 30, 60, 60 の倍数が指定可能(default 60)
  • run_as_user: CloudWatch エージェントの実行ユーザ(default root)
  • debug: CloudWatch エージェントの詳細な実行ログを吐き出すが否か(default false)

    CloudWatch Metrics でメトリクスが取れておらず CloudWatch エージェントが動いてなさそうな場合に true にして確認する際に有効です

metrics セクション

{
  "metrics": {
    "append_dimensions": {
      "ImageId": "${aws:ImageId}",
      "InstanceId": "${aws:InstanceId}",
      "InstanceType": "${aws:InstanceType}",
      "AutoScalingGroupName": "${aws:AutoScalingGroupName}"
    },
    "aggregation_dimensions": [
      ["AutoScalingGroupName"],
      ["InstanceId", "InstanceType"],
      []
    ],
    "metrics_collected": {
      "cpu": {
        "resources": ["*"],
        "measurement": ["usage_system"]
      }
    }
  }
}

このセクションが肝です。 metrics セクションでは取得したいカスタムメトリクスを指定します。

  • metrics_collected: 必須 収集するメトリクスを指定

    cpu, disk, diskio, swap, mem, net, netstat, processes, procstat はそのまま使えますが collectd, statsd はそれぞれ collectd, StatsD をインストールし設定する必要があります。

  • append_dimensions: メトリクスに追加するディメンション

    ディメンションは CloudWatch Metrics でメトリクスを探す際にフィルタリングのように機能します。

  • aggregation_dimensions: 集約するディメンションを指定

    例えば [["AutoScalingGroupName"]] とした場合 AutoScalingGroupName ディメンションを集約して CloudWatch Metrics からは1つのメトリクスとして見ることができます。

CloudWatch エージェントで収集できるメトリクス一覧は Metrics Collected by the CloudWatch Agent - Amazon CloudWatch をご覧ください。

metrics_collected.*.measurement セクションで指定するメトリクスは、完全な名前またはリソースタイプが省略された名前どちらでも指定できます。 例えば、memory フィールド内の mem_used_percentused_percent と、 disk フィールド内の used_percentdisk_used_percent と書くことができます。

logs セクション

{
  "logs": {
    "logs_collected": {
      "files": {
        "collect_list": [
          {
            "file_path": "/var/log/syslog",
            "log_group_name": "/my/log/group/syslog"
          }
        ]
      }
    }
  }
}

ログを収集し CloudWatch Logs へ送信します。 公式ドキュメントには logs.log_stream_name は必須と書いてありますがなくても問題ありませんでした。

指定したロググループがない場合は自動で作成されます。 retention を設定したい場合は予めロググループを作成しておくか、自動作成後に PutRetentionPolicy API を実行する必要があるでしょう。

procstat プラグイン

CloudWatch エージェントには procstat, collectd, StatsD プラグインがあります。

collectdStatsD は CloudWatch エージェントと同様にシステムリソースデータを収集するデーモンです。 CloudWatch エージェントだけで収集できないデータがある場合や、既に collectd または StatsD を使用しておりカスタムメトリクスを登録したい場合はこれらのプラグインを使うとよいかもしれません。

procstat プラグインは何かインストールする必要もなくそのまま使うことができます。 プロセス毎のデータを取得することができ、pid_file, exe, pattern で収集したいプロセスを指定します。

pid_file

プロセス ID が格納されたファイルのパスを指定します。 以下では Nginx のプロセスのメモリ使用量を収集します。

{
  "metrics": {
    "metrics_collected": {
      "procstat": [
        {
          "pid_file": "/var/run/nginx.pid",
          "measurement": ["memory_rss"]
        }
      ]
    }
  }
}

1プロセスしか監視できないので、収集対象のプロセスがマルチプロセスの場合(Apache HTTP MPM prefork/workers や Puma Clustered mode, uWSGI multiple workers 等)は以下の exe または pattern を使ったほうがよいでしょう。

exe

プロセス名を正規表現で指定します。 pgrep <pattern> にマッチするプロセスが対象です。 以下では pgrep nginx にマッチするプロセスのプロセス ID とプロセス数を収集します。

{
  "metrics": {
    "metrics_collected": {
      "procstat": [
        {
          "exe": "nginx",
          "measurement": ["pid", "pid_count"]
        }
      ]
    }
  }
}

pattern

プロセスのフルコマンドを正規表現で指定します。 pgrep -f <pattern> にマッチするプロセスが対象です。 pgrep コマンドのオンラインマニュアル(man pgrep) にもある通り、 exe との違いはプロセス実行時のオプションを含めて検索している点です。

以下では pgrep -f '/var/lib/libvirt/dnsmasq/default.conf' にマッチするプロセスの CPU 使用時間の割合を収集します。 例えば /usr/sbin/dnsmasq --conf-file=/var/lib/libvirt/dnsmasq/default.conf のように起動しているプロセスが収集対象になります。

{
  "metrics": {
    "metrics_collected": {
      "procstat": [
        {
          "pattern": "/var/lib/libvirt/dnsmasq/default.conf",
          "measurement": ["cpu_usage"]
        }
      ]
    }
  }
}

ちなみに exepattern の違いは AWS 公式ドキュメントより telegraf/plugins/inputs/procstat を読んだほうが理解が早かったです。

設定ファイルの反映

設定ファイルができたら反映します。 json ファイルを toml に変換して CloudWatch エージェントを再起動する必要があります。 json ファイルの扱いが億劫な場合は、未検証ですが toml ファイルを配置して restart すれば動作するかもしれません。

以下では /tmp/config.json にある設定ファイルを toml に変換した後に再起動(-s オプション)します。

/tmp/config.json

{
  "metrics": {
    "metrics_collected": {
      "procstat": [
        {
          "exe": "nginx",
          "measurement": ["cpu_usage"]
        }
      ]
    }
  }
}
$ sudo amazon-cloudwatch-agent-ctl -a fetch-config -c file:/tmp/config.json -s
/opt/aws/amazon-cloudwatch-agent/bin/config-downloader --output-dir /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d --download-source file:/tmp/config.json --mode ec2 --config /opt/aws/amazon-cloudwatch-agent/etc/common-config.toml --multi-config default
Successfully fetched the config and saved in /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d/file_config.json.tmp
Start configuration validation...
/opt/aws/amazon-cloudwatch-agent/bin/config-translator --input /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json --input-dir /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d --output /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.toml --mode ec2 --config /opt/aws/amazon-cloudwatch-agent/etc/common-config.toml --multi-config default
2020/05/14 07:06:49 Reading json config file path: /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d/file_config.json.tmp ...
Valid Json input schema.
I! Detecting runasuser...
No csm configuration found.
No log configuration found.
Configuration validation first phase succeeded
/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent -schematest -config /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.toml
Configuration validation second phase succeeded
Configuration validation succeeded
Redirecting to /bin/systemctl stop amazon-cloudwatch-agent.service
Redirecting to /bin/systemctl restart amazon-cloudwatch-agent.service

設定ファイルが複数ある場合は、上書きされるのを避けるため1つ目のファイルを fetch-config し、2つ目以降のファイルは append-config します。

/tmp/base.json

{
  "agent": {
    "metrics_collection_interval": 10,
    "run_as_user": "root"
  },
  "metrics": {
    "append_dimensions": {
      "InstanceId": "${aws:InstanceId}"
    },
    "metrics_collected": {
      "cpu": {
        "resources": ["*"],
        "measurement": ["usage_system", "usage_user"]
      }
    }
  }
}

/tmp/svc03.json

{
  "metrics": {
    "metrics_collected": {
      "procstat": [
        {
          "pattern": "nginx",
          "measurement": ["memory_rss"]
        }
      ]
    }
  }
}
$ sudo amazon-cloudwatch-agent-ctl -a fetch-config -c file:/tmp/base.json
/opt/aws/amazon-cloudwatch-agent/bin/config-downloader --output-dir /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d --download-source file:/tmp/base.json --mode ec2 --config /opt/aws/amazon-cloudwatch-agent/etc/common-config.toml --multi-config default
Successfully fetched the config and saved in /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d/file_base.json.tmp
Start configuration validation...
/opt/aws/amazon-cloudwatch-agent/bin/config-translator --input /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json --input-dir /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d --output /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.toml --mode ec2 --config /opt/aws/amazon-cloudwatch-agent/etc/common-config.toml --multi-config default
2020/05/14 07:19:54 Reading json config file path: /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d/file_base.json.tmp ...
Valid Json input schema.
I! Detecting runasuser...
No csm configuration found.
No log configuration found.
Configuration validation first phase succeeded
/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent -schematest -config /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.toml
Configuration validation second phase succeeded
Configuration validation succeeded
$ sudo amazon-cloudwatch-agent-ctl -a append-config -c file:/tmp/svc03.json -s
/opt/aws/amazon-cloudwatch-agent/bin/config-downloader --output-dir /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d --download-source file:/tmp/svc03.json --mode ec2 --config /opt/aws/amazon-cloudwatch-agent/etc/common-config.toml --multi-config append
Successfully fetched the config and saved in /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d/file_svc03.json.tmp
Start configuration validation...
/opt/aws/amazon-cloudwatch-agent/bin/config-translator --input /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json --input-dir /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d --output /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.toml --mode ec2 --config /opt/aws/amazon-cloudwatch-agent/etc/common-config.toml --multi-config append
2020/05/14 07:31:05 Reading json config file path: /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json ...
/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json does not exist or cannot read. Skipping it.
2020/05/14 07:31:05 Reading json config file path: /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d/file_base.json ...
2020/05/14 07:31:05 Reading json config file path: /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d/file_svc03.json.tmp ...
Valid Json input schema.
I! Detecting runasuser...
No csm configuration found.
No log configuration found.
Configuration validation first phase succeeded
/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent -schematest -config /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.toml
Configuration validation second phase succeeded
Configuration validation succeeded
Redirecting to /bin/systemctl stop amazon-cloudwatch-agent.service
Redirecting to /bin/systemctl restart amazon-cloudwatch-agent.service
$ cat /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.toml
[agent]
  collection_jitter = "0s"
  debug = false
  flush_interval = "1s"
  flush_jitter = "0s"
  hostname = ""
  interval = "10s"
  logfile = "/opt/aws/amazon-cloudwatch-agent/logs/amazon-cloudwatch-agent.log"
  metric_batch_size = 1000
  metric_buffer_limit = 10000
  omit_hostname = false
  precision = ""
  quiet = false
  round_interval = false

[inputs]

  [[inputs.cpu]]
    fieldpass = ["usage_system", "usage_user"]
    percpu = true
    totalcpu = true
    [inputs.cpu.tags]
      "aws:StorageResolution" = "true"
      metricPath = "metrics"

  [[inputs.procstat]]
    fieldpass = ["memory_rss"]
    pattern = "nginx"
    pid_finder = "native"
    [inputs.procstat.tags]
      "aws:StorageResolution" = "true"
      metricPath = "metrics"

[outputs]

  [[outputs.cloudwatch]]
    force_flush_interval = "60s"
    namespace = "CWAgent"
    region = "ap-northeast-1"
    tagexclude = ["metricPath"]
    [outputs.cloudwatch.tagpass]
      metricPath = ["metrics"]

CloudWatch Metrics

上記で設定した CloudWatch エージェントを動作させて、実際に CloudWatch Metrics からカスタムメトリクスとして登録したデータを確認してみましょう。

以下の設定ファイルの場合だとします。

/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.toml

[agent]
  collection_jitter = "0s"
  debug = false
  flush_interval = "1s"
  flush_jitter = "0s"
  hostname = ""
  interval = "10s"
  logfile = "/opt/aws/amazon-cloudwatch-agent/logs/amazon-cloudwatch-agent.log"
  metric_batch_size = 1000
  metric_buffer_limit = 10000
  omit_hostname = false
  precision = ""
  quiet = false
  round_interval = false

[inputs]

  [[inputs.cpu]]
    fieldpass = ["usage_system", "usage_user"]
    percpu = true
    totalcpu = true
    [inputs.cpu.tags]
      "aws:StorageResolution" = "true"
      metricPath = "metrics"

  [[inputs.procstat]]
    fieldpass = ["memory_rss"]
    pattern = "nginx"
    pid_finder = "native"
    [inputs.procstat.tags]
      "aws:StorageResolution" = "true"
      metricPath = "metrics"

[outputs]

  [[outputs.cloudwatch]]
    force_flush_interval = "60s"
    namespace = "CWAgent"
    region = "ap-northeast-1"
    tagexclude = ["host", "metricPath"]
    [outputs.cloudwatch.tagpass]
      metricPath = ["metrics"]

[processors]

  [[processors.ec2tagger]]
    ec2_metadata_tags = ["InstanceId"]
    refresh_interval_seconds = "2147483647s"
    [processors.ec2tagger.tagpass]
      metricPath = ["metrics"]

f:id:maya2250:20201109200459p:plain CPU 利用率 が cpu_usage_system, cpu_usage_user メトリクスで取得できていることがわかります。 CPU 利用率がきちんと取れているか確認するために 16:55 から 16:58 の間で stress-ng コマンドを使用し負荷を掛けています。

使用例

EC2 インスタンスがサーバとして機能している場合の CloudWatch エージェントの設定例を紹介します。

Nginx

  • Nginx は初期設定
  • Nginx プロセスの CPU、メモリ使用量を取得しカスタムメトリクスとして CloudWatch へ登録
  • Nginx のログと CloudWatch エージェントのログを CloudWatch Logs へ送信
{
  "agent": {
    "metrics_collection_interval": 10,
    "logfile": "/var/log/amazon-cloudwatch-agent/amazon-cloudwatch-agent.log"
  },
  "metrics": {
    "namespace": "/prod/svc01",
    "metrics_collected": {
      "procstat": [
        {
          "pattern": "nginx",
          "measurement": ["cpu_usage", "memory_rss"],
          "metrics_collection_interval": 10
        }
      ]
    },
    "append_dimensions": {
      "InstanceId": "${aws:InstanceId}"
    }
  },
  "logs": {
    "logs_collected": {
      "files": {
        "collect_list": [
          {
            "file_path": "/var/log/nginx/access.log",
            "log_group_name": "/prod/svc01/nginx",
            "log_stream_name": "{instance_id}",
            "timestamp_format": "%d/%b/%Y:%H:%M:%S %z",
            "multi_line_start_pattern": "{timestamp_format}",
            "auto_removal": true
          },
          {
            "file_path": "/var/log/nginx/error.log",
            "log_group_name": "/prod/svc01/nginx",
            "log_stream_name": "{instance_id}",
            "timestamp_format": "%Y/%m/%d %H:%M:%S",
            "multi_line_start_pattern": "{timestamp_format}",
            "auto_removal": true
          },
          {
            "file_path": "/var/log/amazon-cloudwatch-agent/amazon-cloudwatch-agent.log",
            "log_group_name": "/prod/svc01/amazon-cloudwatch-agent",
            "log_stream_name": "/prod/svc01/amazon-cloudwatch-agent",
            "timestamp_format": "%Y-%m-%dT%H:%M:%S",
            "multi_line_start_pattern": "{timestamp_format}",
            "auto_removal": true
          }
        ]
      }
    }
  }
}

CloudWatch エージェントの実行ログをデフォルトの /opt/aws/amazon-cloudwatch-agent/logs/amazon-cloudwatch-agent.log とは別に /var/log/amazon-cloudwatch-agent/amazon-cloudwatch-agent.log へ吐き出すようにしています。 これはデフォルトのログファイルには amazon-cloudwatch-agent-ctl で CloudWatch エージェントを操作した際のログも吐き出されるようになっており、 timestamp の形式が違い正しい timestamp_format, multi_line_start_pattern が指定できないための措置です。

/opt/aws/amazon-cloudwatch-agent/logs/amazon-cloudwatch-agent.log

2020/05/15 02:22:38 Reading json config file path: /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d/file_config.json ...
2020/05/15 02:22:38 I! Detected runAsUser: root
2020/05/15 02:22:38 I! Change ownership to root:root
2020-05-15T02:22:38Z I! cloudwatch: get unique roll up list []
2020-05-15T02:22:38Z I! cloudwatch: publish with ForceFlushInterval: 1m0s, Publish Jitter: 37s
2020-05-15T02:22:38Z I! Starting AmazonCloudWatchAgent (version 1.237768.0)

こちらの検証環境を作成する Terraform は こちら に置いておきます。

さいごに

CloudWatch エージェントを導入して EC2 インスタンスのリソースデータをカスタムメトリクスとして登録し、 CloudWatch Metrics で確認する方法を紹介しました。 今回は json ファイルから設定しましたが、json 設定ファイルを Systems Manager Parameter Store に格納して Systems Manager Run Command を使えばインスタンスにログインする必要もなく、複数台の設定も容易だと思います。

AWS のサービスを使って収集・監視・通知の仕組みを導入するのであれば、これに加えて CloudWatch Alarms や SNS が必要になってきます。 まずは収集の部分だけでも CloudWatch エージェントを導入してみてはどうでしょうか。

References

Ubuntu 20.04 を LVM on LUKS でインストールする

メインの開発マシンには Dell XPS 13 9380 に Ubuntu 19.04 (Disco Dingo) をインストールし Ubuntu 19.10 (Eoan Ermine) へ apt-get dist-upgrade したものを使用していました。 1年近く使っていると不要になったパッケージや設定ファイル、キャッシュが残ったままで本来必要だったものってなんだっけ?状態になっていました。 Focal Fossa がリリースされたのもあって、これを機に Ubuntu 20.04 (Focal Fossa) を真っ更な状態からインストールします。

Disco Dingo をインストールしたときにルートパーティションとは別に /var パーティションに 15 GB 割り当てていました。 この /var が意外と容量を取りまして、docker image や KVM image はデフォルトで /var 内に置くようになっており、 すぐに容量いっぱいになったので一時的 image を home directory に保存するよう設定していました。 マシン依存で docker や KVM の設定を書き換えたり、細かいとこではログファイルの格納場所を変更する必要がでてくる可能性もあって、 その都度設定ファイルを書き直す必要が出てくるのではという不安がありました。 なので、今回は LVM でパーティショニングしインストール後でもパーティションサイズを変更できるようにします。

万一マシンを紛失した場合のことを考えて、LUKS でディスク暗号化もします。 どういった構成にするのか、ディスク暗号化の方法等は ArchWiki(dm-crypt/Encrypting an entire system - ArchWiki)がとても参考になりました。

この記事では Ubuntu 20.04 (Focal Fossa) を LVM on LUKS でインストールする方法を紹介します。 LVM や UEFI, LUKS とは、ということは別の記事で紹介したいと思います。

また、Ubuntu を Full Disk Encryption でインストールする方法については Ubuntu の公式 WikiFull_Disk_Encryption_Howto_2019 - Community Help Wiki)が参考になりました。 今回紹介する方法はこちらを基本に自分用環境に少しカスタマイズしている、という認識で読んでもらえたらと思います。

やること

  • Ubuntu 20.04 (Focal Fossa) のインストール
  • LVM on LUKS + /boot 暗号化
  • UEFI Boot のみで、 Legacy Boot は非対応
  • GPT + EFI-SP
  • KVM + libvirt 検証環境で動作確認

最終目的は XPS 13 9380 にインストールすることですが、まずは検証環境でインストールして動作確認した後に XPS 13 9380 にインストールしますので、KVM + libvirt 環境での紹介になります。

実機でも基本的なやり方は変わらずインストールできました。 LVM の構成やデバイス名、割当サイズが変わるくらいでした。

パーティション構成

+----------------------+-----------------------+-------------------------------+
| EFI System Partition | Boot Partition        | Linux LUKS Partition          |
| (ef00)               | (8300)                | (8309)                        |
|                      |                       |                               |
| /dev/vda1            | /dev/vda2             | /dev/vda3                     |
|                      |                       |                               |
| unencrypted          | encrypted             | encrypted                     |
|                      +-------------------------------------------------------+
|                      | /dev/mapper/luks_boot | /dev/mapper/luks_vda3         | LUKS Mapping
|                      +---------------------------------------+---------------+
|                      |                       | /dev/vg1/swap | /dev/vg1/root | LVM
|                      |                       +-------------------------------+
| /boot/efi            | /boot                 | swap          | /             | Mount Point
+----------------------+-----------------------+---------------+---------------+

大きく3つのパーティションに分けます。

/dev/vda2, /dev/vda3 を暗号化し、dm-crypt でマッピングを作成します。 更にルートパーティション用のマッピング/dev/mapper/luks_vda3)を LVM の PV とし、VG を作成し swap, / マウントポイント用の LV を作成します。

上記の構成でインストールしていきます。 ちなみに実機インストールでは swap, / に加えて /var, /home 用の LV を追加しました。 ここは各々の構成に合わせて設定すれば良いと思います。

インストール方法

KVM + libvirt 検証 VM を作るところからインストールまで紹介します。

※ 既存環境を破壊するコマンドが出てきますので、各々の環境に合わせて必要なコマンドを実行してください。

前準備

  1. Desktop installer ISO image を公式(Ubuntu 20.04 LTS (Focal Fossa))からダウンロードします。

  2. VM を用意します。

    設定例

    f:id:maya2250:20201109195654p:plain f:id:maya2250:20201109195704p:plain f:id:maya2250:20201109195712p:plain f:id:maya2250:20201109195720p:plain f:id:maya2250:20201109195727p:plain f:id:maya2250:20201109195734p:plainUEFI Boot mode で起動させるために Customize configuration before install にチェックを入れて Overvier > Details > Firmware: UEFI x86_64... を選択する必要があります

    f:id:maya2250:20201109195741p:plain UEFI Boot mode だとこのような真っ黒い画面の GRUB boot loader が表示されます。

f:id:maya2250:20201109195748p:plain この画面が開けたら準備完了です。

インストール

コマンドをまとめたものと実行・出力例は コマンド実行例 に記載しておきます。 どうしてもうまくインストールできない、コマンドの出力が合わない等あればこちらを参考にしてください。

パーティショニング

Installation type で Erase disk and install Ubuntu を選択することで、パーティション設定とディスク暗号化の設定をおまかせすることができます。 この GUI からのインストールだとパーティションのカスタマイズと LVM の設定はできないので、インストール前に手動でパーティション設定・ディスク暗号化の設定を行います。

Try Ubuntu ボタンをクリックしてターミナルを開きます。

f:id:maya2250:20201109195902p:plain root 権限が必要なコマンドばかり実行するので root になります。

sudo -i

暗号化デバイスを open するのに必要なパスフレーズとデバイス名を後で使い回せるよう環境変数に入れておきます。 変数でもいいのですが、後々 chroot して引き継げるよう環境変数にしてます。 DEV, DM ではインストール先デバイスを指定します。

read -sr PASSPHRASE
export PASSPHRASE
export DEV="/dev/vda"
export DM="vda"

パーティション全削除して新たにパーティションを切ります。 同じデバイスに他 OS がある場合は注意してください。 ここでは必要最小限だけパーティションを切ります。

sgdisk コマンドでパーティションを作成しますが、GPT パーティションが作れればいいので fdisk, gparted 等でも作成できると思います。

sgdisk --zap-all "${DEV}"
sgdisk --new=1:0:+100M --change-name=1:"EFI System" --typecode=1:ef00 "${DEV}"
sgdisk --new=2:0:+500M --change-name=2:"Boot"       --typecode=2:8300 "${DEV}"
sgdisk --new=3:0:0     --change-name=3:"Linux LUKS" --typecode=3:8309 "${DEV}"
sgdisk --print "${DEV}"

LUKS の初期化をして上記で作成したパーティションの暗号化をします。 luks2 は未対応です。

--key-file, -d はファイルからパスフレーズを読み込むオプションです。 対話モードだと都度入力するのが大変なので、 - を指定して標準入力から読み込むようにします。

--batch-mode, -q では対話モードで確認をしないようにします。

printf %s "${PASSPHRASE}" | cryptsetup luksFormat --type=luks1 --key-file - --batch-mode "${DEV}2"
printf %s "${PASSPHRASE}" | cryptsetup luksFormat --type=luks1 --key-file - --batch-mode "${DEV}3"

初期化された LUKS パーティションを open します。 LUKS デバイスとのマッピングを作成して /dev/mapper で使えるようにします。

printf %s "${PASSPHRASE}" | cryptsetup open -d - "${DEV}2" luks_boot
printf %s "${PASSPHRASE}" | cryptsetup open -d - "${DEV}3" "luks_${DM}3"

マッピングできているか確認します。 luks_boot, luks_vda3 があれば問題なくマウントされています。

ls -l /dev/mapper/
control  luks_boot  luks_vda3

作成したパーティションをフォーマットします。 EFI boot パーティション/dev/vda1)は VFAT 32 bit でフォーマットします。

EFI boot パーティションは USB ドライブ等の removable disk からでもブートできるようにするため FAT12 または FAT16, FAT32 でフォーマットされています。

mkfs.ext4 -L boot /dev/mapper/luks_boot
mkfs.vfat -F 32 -n EFI-SP "${DEV}1"

LVM の PV, VG を作成します。

pvcreate /dev/mapper/"luks_${DM}3"
vgcreate vg1 /dev/mapper/"luks_${DM}3"

LVM の LV を作成します。

lvcreate -L 4G -n swap vg1
lvcreate -l 100%FREE -n root vg1

以上まで問題なければ Install Ubuntu 20.04 LTS を開いてインストールを開始します。

Ubuntu インストール

設定例

f:id:maya2250:20201109195924p:plain f:id:maya2250:20201109195932p:plain f:id:maya2250:20201109195941p:plain f:id:maya2250:20201109195950p:plain 作成されたパーティションをにマウントポイントを割り当ててフォーマットします

f:id:maya2250:20201109200006p:plain f:id:maya2250:20201109200015p:plain f:id:maya2250:20201109200023p:plain f:id:maya2250:20201109200032p:plain パーティションへの設定が終わったら Device for boot loader installation/dev/vda を選択して Install Now ボタンをクリックします。

f:id:maya2250:20201109200042p:plain f:id:maya2250:20201109200052p:plain f:id:maya2250:20201109200100p:plain f:id:maya2250:20201109200110p:plain

GRUB でディスク暗号化を有効

ユーザ設定後、パッケージインストールや GRUB の設定が自動で進められます。

f:id:maya2250:20201109200138p:plain このときに以下のコマンドを実行します。 これを実行しないと installer が grub-install で入力を求められてインストールが失敗します。

echo "GRUB_ENABLE_CRYPTODISK=y" >> /target/etc/default/grub

f:id:maya2250:20201109200149p:plain

Ubuntu インストール後

インストールが完了するとこのようなポップアップが表示され、テストし続けるか再起動するか選択させられます。

引き続きターミナルからディスク暗号化等の設定をする必要があるので、Continue Testing ボタンをクリックします。

f:id:maya2250:20201109200206p:plain パーティショニング で使用していたターミナルに戻ります。

インストールされた / パーティション/target へマウントします。 もし /etc パーティションを分けているのであればそちらもマウントする必要があります。

mount /dev/mapper/vg1-root /target

proc, sys, devDNS 名前解決のための etc/resolv.conf/target へマウントします。

--rbind オプションではマウント元でマウントしているデバイスをマウントし直します。

for n in proc sys dev etc/resolv.conf; do mount --rbind "/$n" "/target/$n"; done

chroot して /etc/fstab で記載されているデバイスをマウントします。

chroot /target
mount -a

この時点では LUKS の key にはパスフレーズのみ設定している状態です。 後に /etc/crypttab で keyfile から LUKS パーティションを自動マウントできるようにするため keyfile を作成・追加します。

apt install -y cryptsetup-initramfs
echo "KEYFILE_PATTERN=/etc/luks/*.keyfile" >> /etc/cryptsetup-initramfs/conf-hook
echo "UMASK=0077" >> /etc/initramfs-tools/initramfs.conf

mkdir /etc/luks
dd if=/dev/urandom of=/etc/luks/boot_os.keyfile bs=4096 count=1

chmod u=rx,go-rwx /etc/luks
chmod u=r,go-rwx /etc/luks/boot_os.keyfile

printf %s "${PASSPHRASE}" | cryptsetup luksAddKey -d - "${DEV}2" /etc/luks/boot_os.keyfile
printf %s "${PASSPHRASE}" | cryptsetup luksAddKey -d - "${DEV}3" /etc/luks/boot_os.keyfile

LUKS パーティションを自動マウントするよう設定します。

echo "luks_boot UUID=$(blkid -s UUID -o value ${DEV}2) /etc/luks/boot_os.keyfile luks,discard" >> /etc/crypttab
echo "luks_${DM}3 UUID=$(blkid -s UUID -o value ${DEV}3) /etc/luks/boot_os.keyfile luks,discard" >> /etc/crypttab

初期 RAM ディスクを更新します。

update-initramfs -uk all

chroot から抜けて再起動します。

exit
reboot

再起動

再起動が始まると以下のような画面が表示されるので Enter キーを押します。

f:id:maya2250:20201109200222p:plain 起動すると Enter passphrase for hd0,gpt2 (ec4555eb53fe40cb9acae3b2ff65b686): と表示されます。 UUID の部分は環境ごとに異なります。 設定したパスフレーズを入力し、暗号化されている /boot パーティションを使用できる状態にします。 入力しても文字は表示されないので注意してください。

f:id:maya2250:20201109200230p:plain この画面が表示されればインストール完了です。

f:id:maya2250:20201109200237p:plain

はまったこと

誤って /boot パーティションサイズを 100MiB にしていて、 update-initramfs コマンドで初期 RAM disk 作成時に容量超過でコマンド失敗しました。 初期 RAM ディスク(initrd.img-5.4.0-28-generic)は 80 MiB くらいあるので、複数作成しようとすると失敗します。 エラーメッセージ読んても原因がわからず辛かった…

さいごに

Ubuntu 20.04 (Focal Fossa) を LVM on LUKS でインストールする方法を紹介しました。 他の方のブログや公式サイトの情報では細かい手順まで記載してなかったり断片的だったりで一貫した情報が少なく、 本当に可能なのか不安だった中で検証環境と XPS 13 にインストールできたので安心しました。 この記事がお役に立てれば幸いです。

コマンド実行例

Install Ubuntu 20.04 on XPS 13 9380, LVM on LUKS

実行例

ubuntu@ubuntu:~$ dconf write /org/gnome/desktop/input-sources/xkb-options "['ctrl:nocaps']"
ubuntu@ubuntu:~$ sudo -i
root@ubuntu:~# read -sr PASSPHRASE
root@ubuntu:~# echo ${PASSPHRASE}
password
root@ubuntu:~# export PASSPHRASE
root@ubuntu:~# export DEV="/dev/vda"
root@ubuntu:~# export DM="vda"
root@ubuntu:~# sgdisk --zap-all "${DEV}"
Creating new GPT entries in memory.
GPT data structures destroyed! You may now partition the disk using fdisk or
other utilities.
root@ubuntu:~# sgdisk --new=1:0:+100M --change-name=1:"EFI System" --typecode=1:ef00 "${DEV}"
Creating new GPT entries in memory.
Setting name!
partNum is 0
The operation has completed successfully.
root@ubuntu:~# sgdisk --new=2:0:+500M --change-name=2:"Boot"       --typecode=2:8300 "${DEV}"
Setting name!
partNum is 1
The operation has completed successfully.
root@ubuntu:~# sgdisk --new=3:0:0     --change-name=3:"Linux LUKS" --typecode=3:8309 "${DEV}"
Setting name!
partNum is 2
The operation has completed successfully.
root@ubuntu:~# sgdisk --print "${DEV}"
Disk /dev/vda: 125829120 sectors, 60.0 GiB
Sector size (logical/physical): 512/512 bytes
Disk identifier (GUID): 6DDBA433-4A1E-40A7-8123-CCD4E567AF46
Partition table holds up to 128 entries
Main partition table begins at sector 2 and ends at sector 33
First usable sector is 34, last usable sector is 125829086
Partitions will be aligned on 2048-sector boundaries
Total free space is 2014 sectors (1007.0 KiB)

Number  Start (sector)    End (sector)  Size       Code  Name
   1            2048          206847   100.0 MiB   EF00  EFI System
   2          206848         1230847   500.0 MiB   8300  Boot
   3         1230848       125829086   59.4 GiB    8309  Linux LUKS
root@ubuntu:~#
root@ubuntu:~# printf %s "${PASSPHRASE}" | cryptsetup luksFormat --type=luks1 --key-file - --batch-mode "${DEV}2"
root@ubuntu:~# printf %s "${PASSPHRASE}" | cryptsetup luksFormat --type=luks1 --key-file - --batch-mode "${DEV}3"
root@ubuntu:~#
root@ubuntu:~# printf %s "${PASSPHRASE}" | cryptsetup open -d - "${DEV}2" luks_boot
root@ubuntu:~# printf %s "${PASSPHRASE}" | cryptsetup open -d - "${DEV}3" "luks_${DM}3"
root@ubuntu:~# ls /dev/mapper/
control  luks_boot  luks_vda3
root@ubuntu:~#
root@ubuntu:~# mkfs.ext4 -L boot /dev/mapper/luks_boot
mke2fs 1.45.5 (07-Jan-2020)
Creating filesystem with 127488 4k blocks and 127488 inodes
Filesystem UUID: 14ec2569-976f-453d-8261-d3b3d6099140
Superblock backups stored on blocks:
    32768, 98304

Allocating group tables: done
Writing inode tables: done
Creating journal (4096 blocks): done
Writing superblocks and filesystem accounting information: done

root@ubuntu:~# mkfs.vfat -F 32 -n EFI-SP "${DEV}1"
mkfs.fat 4.1 (2017-01-24)
root@ubuntu:~#
root@ubuntu:~# pvcreate /dev/mapper/"luks_${DM}3"
  Physical volume "/dev/mapper/luks_vda3" successfully created.
root@ubuntu:~# vgcreate vg1 /dev/mapper/"luks_${DM}3"
  Volume group "vg1" successfully created
root@ubuntu:~#
root@ubuntu:~# lvcreate -L 4G -n swap vg1
  Logical volume "swap" created.
root@ubuntu:~# lvcreate -l 100%FREE -n root vg1
  Logical volume "root" created.
root@ubuntu:~# lvs
  LV   VG  Attr       LSize   Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert
  root vg1 -wi-a----- <55.41g
  swap vg1 -wi-a-----   4.00g
..

..
root@ubuntu:~# echo "GRUB_ENABLE_CRYPTODISK=y" >> /target/etc/default/grub
..

..
root@ubuntu:~# mount /dev/mapper/vg1-root /target
root@ubuntu:~# for n in proc sys dev etc/resolv.conf; do mount --rbind "/$n" "/target/$n"; done
root@ubuntu:~# chroot /target
root@ubuntu:/# mount -a
root@ubuntu:/# apt install -y cryptsetup-initramfs
Reading package lists... Done
Building dependency tree
Reading state information... Done
cryptsetup-initramfs is already the newest version (2:2.2.2-3ubuntu2).
0 upgraded, 0 newly installed, 0 to remove and 29 not upgraded.
root@ubuntu:/# echo "KEYFILE_PATTERN=/etc/luks/*.keyfile" >> /etc/cryptsetup-initramfs/conf-hook
root@ubuntu:/# echo "UMASK=0077" >> /etc/initramfs-tools/initramfs.conf
root@ubuntu:/# mkdir /etc/luks
root@ubuntu:/# dd if=/dev/urandom of=/etc/luks/boot_os.keyfile bs=4096 count=1
1+0 records in
1+0 records out
4096 bytes (4.1 kB, 4.0 KiB) copied, 0.000639065 s, 6.4 MB/s
root@ubuntu:/# chmod u=rx,go-rwx /etc/luks
root@ubuntu:/# chmod u=r,go-rwx /etc/luks/boot_os.keyfile
root@ubuntu:/# printf %s "${PASSPHRASE}" | cryptsetup luksAddKey -d - "${DEV}2" /etc/luks/boot_os.keyfile
root@ubuntu:/# printf %s "${PASSPHRASE}" | cryptsetup luksAddKey -d - "${DEV}3" /etc/luks/boot_os.keyfile
root@ubuntu:/# echo "luks_boot UUID=$(blkid -s UUID -o value ${DEV}2) /etc/luks/boot_os.keyfile luks,discard" >> /etc/crypttab
root@ubuntu:/# echo "luks_${DM}3 UUID=$(blkid -s UUID -o value ${DEV}3) /etc/luks/boot_os.keyfile luks,discard" >> /etc/crypttab
root@ubuntu:/# update-initramfs -uk all
update-initramfs: Generating /boot/initrd.img-5.4.0-28-generic
update-initramfs: Generating /boot/initrd.img-5.4.0-26-generic
root@ubuntu:/# exit
root@ubuntu:~# reboot

参考

CKA, CKAD 受験記録

CKA(Certified Kubernetes Administrator), CKAD(Certified Kubernetes Application Developer)という Kubernetes の認定資格があります。 Cloud Native Computing Foundation(CNCF) が提供しており、これまでの CKA の受験者数は 2019年3月時点で 9000 人以上だと言われています。 今回これら CKA, CKAD に合格したので、それまでにやったことを紹介します。

受験した CKA, CKAD はいずれも curriculum v1.17 です。

試験までにやったこと

Candidate Handbookcncf/curriculum を熟読

Udemy の Certified Kubernetes Administrator (CKA) with Practice Tests, Kubernetes Certified Application Developer (CKAD) with Tests コース

  • 1週目 解説動画 + KODEKLOUD の Practice Test + Mock Test
  • 2週目 Practice Test + Mock Test

公式ドキュメント

  • Tasks > Administrator を流し読みした
  • VM 立てて kubeadm で master + worker ノード構成でクラスタ作った

mmumshad/kubernetes-the-hard-wayVagrant + Virtualbox 版)

  • 1週目 コピペでクラスタ構築
  • 2周目 コマンドの意味を理解しつつコピペまたは写経クラスタ構築 本番が master 1 node 構成だったので それに合わせて master-2 は除いた

kubectl explain <resource> でどんなパラメタがあるか流し読み

当日準備

自宅で受験しました。 疑われては面倒なのでカメラに余計なものが映らないよう部屋の整理整頓をしました。

机上には以下の物のみ置くようにしました。

  • ラップトップ(Dell XPS 13 9380)
  • ディスプレイ + カメラ(Elecom 外付け USB カメラ)
  • USB-C アダプタ
  • マウス
  • 電源コード

開始15分前に ポータル画面 へアクセスしました。 数分経ってからリロードしても Take Exam のステータスが変わらなかったのでどうしようかなと思いつつ、 ハンドブックにあったリンクから psi の試験ページへ飛べたのを思い出しました。 あやふやですが compatibility check tool のページを開くと試験前のページへリダイレクトされて Take Exam という緑色のボタンがあったので、それをクリックして試験ページを開いた記憶があります。 他の受験者のブログでもここのステータスが試験直前になっても変わらないってあったので、 ここだけ見ていても駄目なのかもしれないです。

受験の流れ

CKA, CKAD いずれも試験開始までの流れは同じです。 順番はあやふやですが覚えている限りチャットで以下のやり取りをしました。

  • 「デスクトップを共有してください」
  • 「カメラを共有してください。〇〇をクリックしカメラの映像を確認してください。」
  • 「Identification を見せてください」 パスポートとクレジットカードを同時にウェブカメラで写した
  • 「机全体をカメラで写してください」
  • 「正面を見せて下さい」
  • 「このページを開いているタブ以外のタブを閉じてください」
  • chrome 以外の起動しているアプリを閉じてください」
  • 「OS は何ですか?」 「Ubuntu 19.10」 と答えた
  • 「System Monitor を開いてください」 System Monitor ってなんだ…って思って chrome 上にあるものか?と考えつつわからなかったので 「ok?」 と聞くと「まだ開かれてないよ」って言われたので、何かなーってところで プロセス見たいのかと気付き、ランチャーからシステムモニターを開いた
  • 「System Monitor を開いたままにしてください」 多分変なプロセスが立ってないかを数分置いて確認したいんだと思う
  • 諸注意が流れる
  • 「他に質問は無いですか?」
  • 問題文とターミナルが表示され試験開始

ハプニング

試験開始から5分経過したあたりで誤って試験タブを閉じてしまいました。 普段ターミナル操作では、カーソル前の単語を削除するのに C-wキーバインドを使います。 左に問題文、右にターミナルという構成になっていて 恐らく左の問題文にカーソルがあたっているときに C-w を押してしまったのだと思います。 焦りつつも直ぐに復帰すると 「Welcome back!」とチャットメッセージが着て 続けられるのかなと不安でしたが、問題なく問題文・ターミナルに再接続できたのでそのまま続行しました。 それ以降は C-w を叩かないように気をつけました(まぁ何回か使ったけど…)。 一時的に Chrome のショートカットを無効にできればよかったのですが、Chrome 拡張でできるかも‥?(How to disable a Chrome keyboard shortcut

誤って余分なタブを開いてしまったのが 3 回ありましたが、ページが表示されないうちに閉じました。 何も言われなかったので多分大丈夫。

所感

使用するツールが普段と違うのと変なコマンド叩いたら試験のセッションが終わる… という不安を抱えながらターミナル操作するのが辛かったです。 普段の開発には guake + tmux を使っていて、 試験でも同様の環境で操作できたら…と思っていたけれど、以下の理由から止めました。

  • tmux と chromeキーバインドの衝突。 特に prefix を C-t にしているので、都度タブが開かれるのでは…と心配していました。 なるべく不安ごとはなくしたいので、ここややむなし…
  • dotfiles を使いたかったけど、勝手に外部からインストールしていいんだろうか…? 確か handbook に禁止と書いてあったような…

C-d でログアウトしてターミナルセッションを閉じないように気をつけるのも辛かったです。 他ノードへ ssh login してログアウトする際にはログイン中のホスト名を数回目視確認して C-d を押してました。

結果 CKA はスコア 96% で合格できました。 全問理解できたけど単純に自分が書き方を知らなかったために確実に解けてない問題が1つ、 出力形式が不安な問題が2つありました。

CKAD のスコアは 78% でした。 CKA 受験から2週間後に受けたのと、あれくらいの難易度ならーと高をくくっていたのであまりスコアはよくありませんでした。 CKA が3時間24問、CKAD が2時間19問で、1問あたりに掛けられる時間は割り算で CKA: 7.5 分, CKAD: 6.3 分です。 それさえも事前に調べてなかったので1問目からゆっくり解いて、全部解き終わったときには残り時間が5分でした。 完全に時間配分をミスってしまい見直しができなかったので単純ミスで数問落としたかと‥。 試験終了時は正直落ちてもおかしくはないなと思いましたが、受かってホッとしました。

Emacs で Flycheck, ESLint の設定

TL;DR

やりたいこと

Emacs で ESLint を動作させます。 ESLint の実行ファイルはプロジェクト毎にインストールされたもの(${PROJECT_ROOT}/node_modules/.bin/eslint)を使用し、 ESLint の設定ファイル(${PROJECT_ROOT}/.eslintrc)のルールを用いて lint します。

設定方法

Flycheck で使用している linter を確認するため (flycheck-verify-setup) 関数を使います。 ちなみに JavaScript を編集する際には js2-mode を使用しています。

Syntax checkers for buffer index.js in js2-mode:

No checker to run in this buffer.

Checkers that are compatible with this mode, but will not run until properly configured:

  javascript-eslint (disabled)
    - may enable:  Automatically disabled!
    - executable:  Not found
    - config file: missing or incorrect
...

javascript-eslint (disabled) と表示されているので、Flycheck で ESLint が使えない状態であることが確認できました。

これを修正するには、まずは add-node-modules-path をインストールして ESLint 実行ファイルを Emacs に認識させます。

init.el

;...
(use-package add-node-modules-path
  :hook (js-mode js2-mode))
;...

再びflycheck-verify-setupしてみると

Syntax checkers for buffer index.js in js2-mode:

No checker to run in this buffer.

Checkers that are compatible with this mode, but will not run until properly configured:

  javascript-eslint (disabled)
    - may enable:  Automatically disabled!
    - executable:  Found at ${PROJECT_ROOT}/node_modules/.bin/eslint
    - config file: missing or incorrect
...

executable: Found at ${PROJECT_ROOT}/node_modules/.bin/eslint とあるので、eslint の実行ファイルの設定は問題ありませんが、 .eslintrc を見つけられてない状態であることが確認できました。 しかし、既にプロジェクトルートには .eslintrc を配置しています。

こちらの Issue(Proposal: eslint --locate-config · Issue #7719 · eslint/eslint) を参考にして、 そういえば .eslintrcextends: airbnb-base しているので依存パッケージあるなと思い、インストールしているパッケージを確認してみます。

$ npm ls --depth 0
example@0.0.1 ${PROJECT_ROOT}
├── clean-webpack-plugin@3.0.0
├── copy-webpack-plugin@5.0.3
├── UNMET PEER DEPENDENCY eslint@6.0.1
├── eslint-config-airbnb-base@13.2.0
├── eslint-loader@2.2.1
├── eslint-plugin-import@2.18.0
├── webpack@4.35.2
└── webpack-cli@3.3.5

npm ERR! peer dep missing: eslint@^4.19.1 || ^5.3.0, required by eslint-config-airbnb-base@13.2.0

UNMET PEER DEPENDENCY, eslint@^4.19.1 || ^5.3.0, required by eslint-config-airbnb-base@13.2.0 と表示されているので、eslint-config-airbnb-base が依存してる ESLint の version があってないんだなと気付きました。

package.json の eslint の version を ^5.3.0 にして npm install し、 Emacsflycheck-verify-setup したところ、

Syntax checkers for buffer content.js in js2-mode:

First checker to run:

  javascript-eslint
    - may enable:  yes
    - executable:  Found at ${PROJECT_ROOT}/node_modules/.bin/eslint
    - config file: found

Checkers that are compatible with this mode, but will not run until properly configured:
...

とうまく設定されていることが確認できました。 きちんと Flycheck も動いて、Emacs に波線等が表示されるようになりました。

恐らく ↑ で .eslintrc を配置していたけど、Not Found って出ていたのは、設定ファイルが正しく認識されていなかったからなのではと推測‥

Emacs のフォントを変更する

多環境で Emacs を使う際に、フォントの設定をしていないと等幅フォントが設定されてコードが読み書きしづらいということが起きます。 今回はそういったことを避けるため、Emacs でのフォントの設定方法を紹介します。

検証環境

$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=19.10
DISTRIB_CODENAME=eoan
DISTRIB_DESCRIPTION="Ubuntu 19.10"
$ emacs --version
GNU Emacs 26.3
Copyright (C) 2019 Free Software Foundation, Inc.
GNU Emacs comes with ABSOLUTELY NO WARRANTY.
You may redistribute copies of GNU Emacs
under the terms of the GNU General Public License.
For more information about these matters, see the file named COPYING.

変更方法

まずは使用可能フォントを確認します。 Ricty フォントがあるか調べます。

$ fc-list : family | grep -i ricty
Ricty Discord
Ricty Diminished Discord
Ricty
Ricty Diminished

Ricty の他に Ricty Diminished もインストールされていることがわかりました。 もし使いたいフォントが無ければインストールしましょう。

等など。

Emacs で認識されているフォントを確認します。 font-family-list 関数を評価して、Emacs で使えるフォントを表示しましょう。

  • *scratch* バッファで (dolist (x (font-family-list)) (print x)) を評価

    ```emacs (dolist (x (font-family-list)) (print x)) ; Enter C-j

    "newspaper"

    "gothic" ... ```

  • M-: (eval-expression)で (member "Ricty" (font-family-list)) を実行して nil であれば Ricty フォントは使用できない

等の方法があります。 フォントがインストールされており Emacs でも認識できていることが確認できたら、 設定ファイル(init.el)に以下を追記します。

init.el

; Ricty 
(when (member "Ricty" (font-family-list))
  (add-to-list 'default-frame-alist '(font . "Ricty 11")))

これを追記し C-M-xeval-defun)で評価しても即時反映はされません。 即時確認するには *scratch* バッファや M-: 等で (set-default-font "Ricty") を評価してやるとよいです。 もちろん Emacs を再起動して確認する方法もあります。

フォントの指定方法

GNU Emacs Manual(Fonts - GNU Emacs Manual)によるとフォントの指定の仕方は3通りあるそうです。

Fontconfig pattern

Monospace
Monospace-12
Monospace-12:bold
DejaVu Sans Mono:bold:italic
Monospace-12:weight=bold:slant=italic

GTK font pattern

Monospace 12
Monospace Bold Italic 12

XLFD (X Logical Font Description)

-misc-fixed-medium-r-semicondensed--13-*-*-*-c-60-iso8859-1

XLFD は表記が分かりづらいので、Fontconfig か GTK の形式を使うのが良いと思います。

デバッグ

Emacs フォントを設定する際に使用したツール・コマンド等の紹介です。

Emacs

  • describe-char または C-u C-x =: カーソル位置の文字の情報を表示をします。 xft:-PfEd-Ricty-bold-...の部分を見れば使用されているフォントを確認できます

    例:

    ``` position: 2335 of 3645 (64%), column: 22 character: る (displayed as る) (codepoint 12427, #o30213, #x308b) charset: japanese-jisx0208 (JISX0208.1983/1990 Japanese Kanji: ISO-IR-87) code point in charset: 0x246B script: kana syntax: w which means: word category: .:Base, H:2-byte Hiragana, L:Left-to-right (strong), c:Chinese, h:Korean, j:Japanese, |:line breakable to input: type "C-x 8 RET 308b" or "C-x 8 RET HIRAGANA LETTER RU" buffer code: #xE3 #x82 #x8B file code: #xE3 #x82 #x8B (encoded by coding system utf-8-unix) display: by this font (glyph code) xft:-1ASC-Droid Sans Fallback-normal-normal-normal--13----*-0-iso10646-1 (#xBE)

    Character code properties: customize what to show name: HIRAGANA LETTER RU general-category: Lo (Letter, Other) decomposition: (12427) ('る')

    There are 2 overlays here: From 1 to 3646 after-string [Show] face hiwin-face window # From 2321 to 2358 face hl-line priority -50 window #

    There are text properties here: fontified t

    [back] ```

  • describe-font RET <FONT>: 入力されたフォントの fullname, 格納場所等が表示されます

    例:

    name (opened by): -PfEd-Ricty-normal-normal-normal-*-16-*-*-*-*-0-iso10646-1 full name: Ricty:pixelsize=16:foundry=PfEd:weight=normal:slant=normal:width=normal:scalable=true file name: /home/maya/.fonts/Ricty-Regular.ttf size: 16 height: 18 baseline-offset: 0 relative-compose: 0 default-ascent: 0 ascent: 14 descent: 4 average-width: 8 space-width: 8 max-width: 8

  • describe-font-sets RET <FONTSET>: FONTSET 無指定だとデフォルトで現在のフレームで使用されているフォントセットを表示します。

    例:

    Fontset: -1ASC-Liberation Mono-normal-normal-normal-*-13-*-*-*-*-0-fontset-auto1 CHAR RANGE (CODE RANGE) FONT NAME (REQUESTED and [OPENED]) C-@ .. DEL -*-*-*-*-*-*-*-*-*-*-*-*-iso10646-1 .. ʯ (#x80 .. #x2AF) -*-*-*-*-*-*-*-*-*-*-*-*-iso10646-1 [-1ASC-Liberation Mono-normal-normal-normal-*-13-*-*-*-*-0-iso10646-1] ʰ .. ͯ (#x2B0 .. #x36F) -*-*-*-*-*-*-*-*-*-*-*-*-iso10646-1 ...

Shell Command

fc-list コマンド: インストールされているフォントを表示します

  • オプション無指定だとフォントのフルパス表示

    例:

    bash $ fc-list | head -3 /usr/share/fonts/truetype/lato/Lato-Medium.ttf: Lato,Lato Medium:style=Medium,Regular /usr/share/fonts/truetype/noto/NotoSerifHebrew-CondensedLight.ttf: Noto Serif Hebrew,Noto Serif Hebrew Cond Light:style=Condensed Light,Regular /usr/share/fonts/truetype/noto/NotoSans-SemiCondensedExtraLightItalic.ttf: Noto Sans,Noto Sans SemiCondensed ExtraLight:style=SemiCondensed ExtraLight Italic,Italic

  • : の後にパターンを指定して絞り込み; fc-list : family でフォントファミリー表示

    例:

    bash $ fc-list : family | head -3 Noto Sans Canadian Aboriginal,Noto Sans CanAborig Th Noto Serif Sinhala,Noto Serif Sinhala SemiCondensed Black Noto Sans Malayalam,Noto Sans Malayalam SemiCondensed ExtraBold

GUI アプリ

GNOME であれば gnome-font-viewer でインストールされているフォントを GUI アプリで確認できます。

例: アプリランチャーから Fonts を選択、またはターミナルで gnome-font-viewer を実行

f:id:maya2250:20201109195400p:plain
gnome-font-viewer


参考

ls ソースコードを取得する

普段使っているコマンドがどう実装されているのか知りたかったのと、 勉強も兼ねてコードリーディングをしたいと思い、ls コマンドのソースコードを読むことにしました。

まずはソースコードを取得する必要があります。 環境によって様々な方法があると思いますが、ここでは Linux での取得方法を紹介します。 Linux 以外はビルド・実行しておらず未検証のためリンクだけ掲載しています。 環境により実装や依存ライブラリが異なるので、ビルド・実行するのであれば環境にあったソースコードを取得したほうがよいです。

Linux

Linux では ls を含む cp, mkdir 等の基本的なコマンドは coreutils というパッケージ内に含まれていますので、 このパッケージのソースコードを取得します。

Ubuntu

coreutils がどのパッケージリポジトリにあるのか確認します

$ apt show coreutils | grep -i apt-sources
APT-Sources: http://jp.archive.ubuntu.com/ubuntu eoan/main amd64 Packages
# or
$ apt-cache showpkg coreutils | grep -i file
                 File: /var/lib/apt/lists/jp.archive.ubuntu.com_ubuntu_dists_eoan_main_binary-amd64_Packages
                 File: /var/lib/apt/lists/jp.archive.ubuntu.com_ubuntu_dists_eoan_main_binary-i386_Packages
                 File: /var/lib/apt/lists/jp.archive.ubuntu.com_ubuntu_dists_eoan_main_i18n_Translation-ja
                 File: /var/lib/apt/lists/jp.archive.ubuntu.com_ubuntu_dists_eoan_main_i18n_Translation-en

coreutils パッケージは http://jp.archive.ubuntu.com/ubuntu eoan/main amd64 Packages で管理されていることがわかりました 。

デフォルトだとパッケージのソースコードを取得できるように設定されていません。 一時的に /etc/apt/sources.list を変更してソースコードを取得できるようにします。

$ sudo vi /etc/apt/sources.list
...
$ grep "^[^#;]" /etc/apt/sources.list
deb http://jp.archive.ubuntu.com/ubuntu/ eoan main restricted
deb-src http://jp.archive.ubuntu.com/ubuntu/ eoan main restricted  # <- これを追加
deb http://jp.archive.ubuntu.com/ubuntu/ eoan-updates main restricted
deb http://jp.archive.ubuntu.com/ubuntu/ eoan universe
...

ダウンロードします

$ sudo apt-get update -y
$ apt-get source coreutils -q
パッケージリストを読み込んでいます...
5,401 kB のソースアーカイブを取得する必要があります。
取得:1 http://jp.archive.ubuntu.com/ubuntu eoan/main coreutils 8.30-3ubuntu2 (dsc) [2,048 B]
取得:2 http://jp.archive.ubuntu.com/ubuntu eoan/main coreutils 8.30-3ubuntu2 (tar) [5,360 kB]
取得:3 http://jp.archive.ubuntu.com/ubuntu eoan/main coreutils 8.30-3ubuntu2 (diff) [39.6 kB]
5,401 kB を 6秒 で取得しました (950 kB/s)
dpkg-source: info: extracting coreutils in coreutils-8.30
dpkg-source: info: unpacking coreutils_8.30.orig.tar.xz
dpkg-source: info: unpacking coreutils_8.30-3ubuntu2.debian.tar.xz
dpkg-source: info: using patch list from debian/patches/series
dpkg-source: info: applying prefer-renameat2-from-glibc-over-syscall.patch
dpkg-source: info: applying renameatu.patch
dpkg-source: info: applying 61_whoips.patch
dpkg-source: info: applying 63_dd-appenderrors.patch
dpkg-source: info: applying 72_id_checkngroups.patch
dpkg-source: info: applying 80_fedora_sysinfo.patch
dpkg-source: info: applying 85_timer_settime.patch
dpkg-source: info: applying 99_kfbsd_fstat_patch.patch
dpkg-source: info: applying 99_float_endian_detection.patch
$ ls
coreutils-8.30/  coreutils_8.30-3ubuntu2.debian.tar.xz  coreutils_8.30-3ubuntu2.dsc  coreutils_8.30.orig.tar.xz

ダウンロードできました。coreutils-8.30/src/ls.cls の主なソースコードです。

CentOS

コマンド例です。dnf を使って rpm をダウンロード、展開します。

cd /tmp
dnf install -y -q dnf-plugins-core
dnf download -y -q --setopt=strict=0 --source coreutils
rpm2cpio coreutils-8.30-6.el8.src.rpm | cpio -idmdD /tmp/coreutils
cd /tmp/coreutils
tar Jxf coreutils-8.30.tar.xz
vi coreutils-8.30/src/ls.c

UNIX

macOS

FreeBSD

NetBSD

CloudFormation Cross Stack Reference を用いたスタックの作り方

CloudFormation のテンプレートを分割した際のスタック作成・管理方法には Cross Stack Reference と Nested Stack の2通りあります。 そのうちの Nested Stack は 以前の記事 で紹介しました。 今回は Cross Stack Reference の紹介です。

Cross Stack Reference とは

Cross Stack Reference を用いると、スタック作成時に必要な情報を他の既存スタックから取得することができます。

実際に Cross Stack Reference を用いて CloudFormation テンプレートからスタックを作成してみましょう。

テンプレート

VPC, Subnet リソースを作成するテンプレートを用います。

vpc.yml

AWSTemplateFormatVersion: "2010-09-09"
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      EnableDnsSupport: true
      EnableDnsHostnames: true
      CidrBlock: 10.0.0.0/16
Outputs:
  VPCId:
    Description: VPC ID
    Value: !Ref VPC
    Export:
      Name: !Sub ${AWS::StackName}-VPCID

subnet.yml

AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  VPCStackName:
    Type: String
    Default: VPCCrossStackName

Resources:
  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !ImportValue
        Fn::Sub: ${VPCStackName}-VPCID
      CidrBlock: 10.0.0.0/24

便宜上 vpc.yml, subnet.yml をそれぞれ親テンプレート、子テンプレート、 同様にそれらのテンプレートを用いて作成されたスタックもそれぞれ親スタック、子スタックと呼ぶことにします。

これらのテンプレートでは

  1. 親テンプレート: Outputs.Export.Name で参照名を指定
  2. 子テンプレート: Parameters でスタック名を渡し、 Fn::ImportValue 関数を用いて スタック名-VPCIDVPC ID を取得

としています。 ここでは子テンプレートのパラメタにスタック名を指定していますが、 親テンプレートでの出力値で指定した参照名を子テンプレートでそのまま用いても構いません。

スタック作成

実際に Cross Stack Reference を用いたスタックを作成してみましょう。

.
├── subnet.yml
└── vpc.yml

先と同じこれらのテンプレートを使います。

まずは親テンプレートから cross-stack-reference-vpc という名前のスタックを作成します。

$ aws cloudformation deploy --stack-name cross-stack-reference-vpc --template-file ./vpc.yml

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - cross-stack-reference-vpc

f:id:maya2250:20201109200809p:plain 作成できました。 マネコンからも cross-stack-reference-vpc-VPCID という出力値でエクスポートされていることが確認できました。

子テンプレートから cross-stack-reference-subnet という名前のスタックを作成します。

$ aws cloudformation deploy --stack-name cross-stack-reference-subnet --template-file ./subnet.yml --parameter-overrides VPCStackName=cross-stack-reference-vpc

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - cross-stack-reference-subnet

作成できました。

スタック更新

Cross Stack Reference を用いて作成されたスタックの更新にはいくつか注意が必要になります。

リソースを削除する必要のない更新は問題ありません。

diff --git a/vpc.yml b/vpc.yml
index 24f7529..87dca49 100644
--- a/vpc.yml
+++ b/vpc.yml
@@ -4,7 +4,7 @@ Resources:
     Type: AWS::EC2::VPC
     Properties:
       EnableDnsSupport: true
-      EnableDnsHostnames: true
+      EnableDnsHostnames: false
       CidrBlock: 10.0.0.0/16
 Outputs:
   VPCId:

このような親テンプレートは更新できます。

しかし、親テンプレートを更新時リソースを再作成する場合は他リソースが既に依存しているためエラーになります。 子スタックも自動で更新されることはありません。

今回では親テンプレートで VPC を削除し直す必要がある場合が当てはまります。

diff --git a/vpc.yml b/vpc.yml
index 24f7529..8d0402c 100644
--- a/vpc.yml
+++ b/vpc.yml
@@ -5,7 +5,7 @@ Resources:
     Properties:
       EnableDnsSupport: true
       EnableDnsHostnames: true
-      CidrBlock: 10.0.0.0/16
+      CidrBlock: 10.0.1.0/16
 Outputs:
   VPCId:
     Description: VPC ID

このように VPC CIDR を変更するには VPC の再作成が必要となり、 10.0.0.0/16VPC に既に依存している子リソースがあるのでエラーになります。 実際に更新してみると

Export cross-stack-reference-vpc-VPCID cannot be updated as it is in use by cross-stack-reference-subnet

という理由によりスタック更新が失敗しました。

この他にも親スタック更新が失敗する場合があるかと思われますので、しっかりと検証したほうが良いでしょう。

さいごに

今回は Cross Stack Reference を用いたスタックの作成・更新の仕方を紹介しました。

Nested Stack では子スタックの Change Set が確認できませんでしたが、Cross Stack Reference であれば可能です。 しかしデプロイは Cross Stack Reference の方が手間が掛かります。

この状況ではこっちのほうがいいと断言することはできませんが、 どこからどこまでを妥協・許容するのかによるのではないでしょうか。

CloudFormation Nested Stack の作り方

CloudFormation テンプレート書いていると、1枚のテンプレートが大きくなり数百行は優に超えてくるので分割したくなります。 分割するとテンプレートの枚数だけデプロイコマンドを叩く必要があるので シェルスクリプトMakefile を書かないと簡単にデプロイはしにくいです。 テンプレート間の依存関係も考慮した上でデプロイする必要もあります。 そこで Nested Stack でテンプレートを構築し、スタックを作成する方法を紹介します。

Nested Stack とは

分割された CloudFormation Template からスタックを作成・管理するには2通りの方法があります。

今回紹介する Nested Stack とは何でしょうか。 公式ドキュメントより抜粋します。

ネストされたスタックは、他のスタックの一部として作成されたスタックです。ネストされたスタックは、AWS::CloudFormation::Stack リソースを使用して別のスタック内に作成します。

少し分かりづらいですが、親スタックが子スタックを作成するということです。

実際に CloudFormation Nested Stack でスタックを構築してみましょう。

親テンプレートとネットワーク用テンプレート(VPC, Subnet)の2枚を使用します。

テンプレート

.
├── network.yml
└── parent.yml

parent.yml

AWSTemplateFormatVersion: "2010-09-09"

Resources:
  NetworkStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./network.yml

network.yml

AWSTemplateFormatVersion: "2010-09-09"

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 172.16.0.0/16
      EnableDnsSupport: "false"
      EnableDnsHostnames: "false"
      InstanceTenancy: dedicated

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: ""
      CidrBlock: 172.16.0.0/24
      VpcId: !Ref VPC

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Fn::Select:
          - 1
          - Fn::GetAZs: ""
      CidrBlock: 172.16.1.0/24
      VpcId: !Ref VPC

スタックの作り方

本来は親テンプレートの Properties.TemplateURL に S3 へ upload した子テンプレートの URL を指定します。 しかし、子テンプレートを S3 に upload してからさらに親テンプレートも変更するとなると大変です。

そこで package サブコマンド を使います。 親テンプレートの Properties.TemplateURL に子テンプレートの相対パスを指定し、 このコマンドオプションに親テンプレートと upload する S3 bucket name を指定し実行すると、

  1. 子テンプレートを S3 bucket へ upload
  2. 親テンプレートの Properties.TemplateURL を upload した子テンプレートの URL に書き換え

となります。親スタックを作成するには、生成された 2. のテンプレートを使用します。

Nested Stack 作成

実際に Nested Stack を作成してみましょう。

.
├── network.yml
└── parent.yml

先と同じこれらのファイルを使います。

$ aws cloudformation package --template-file ./parent.yml --s3-bucket cfn-nested-stack-demo --output-template-file /tmp/response.yml
Uploading to e4821e88e99c0568aa79739fbaa80f2c.template  631 / 631.0  (100.00%)
Successfully packaged artifacts and wrote output template to file /tmp/response.yml.
Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file /tmp/response.yml --stack-name <YOUR STACK NAME>

package サブコマンドを実行すると、子テンプレートが S3 bucket へ upload されます。

$ aws s3 ls s3://cfn-nested-stack-demo/
2020-04-21 12:39:13        631 e4821e88e99c0568aa79739fbaa80f2c.template
$ aws s3 cp s3://cfn-nested-stack-demo/e4821e88e99c0568aa79739fbaa80f2c.template -
AWSTemplateFormatVersion: '2010-09-09'
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 172.16.0.0/16
      EnableDnsSupport: 'false'
      EnableDnsHostnames: 'false'
      InstanceTenancy: dedicated
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Fn::Select:
        - 0
        - Fn::GetAZs: ''
      CidrBlock: 172.16.0.0/24
      VpcId:
        Ref: VPC
  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Fn::Select:
        - 1
        - Fn::GetAZs: ''
      CidrBlock: 172.16.1.0/24
      VpcId:
        Ref: VPC

作成された親テンプレート /tmp/response.yml は以下のようになります。

AWSTemplateFormatVersion: '2010-09-09'
Resources:
  NetworkStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: https://s3.ap-northeast-1.amazonaws.com/cfn-nested-stack-demo/e4821e88e99c0568aa79739fbaa80f2c.template

子テンプレートを指定する TemplateURL が変わっていることを確認できました。

ではこのテンプレートを使って Nested Stack を作成しましょう。

$ aws cloudformation deploy --template-file /tmp/response.yml --stack-name cfn-nested-stack-demo

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - cfn-nested-stack-demo

作成されました。

Nested Stack が作成されると、子スタックは以下のようになります。

  • 親スタック名-子スタックリソース名-ランダム文字列 というスタック名になる
  • AWS Management Console では子テンプレートに ネストされたNESTED というタグが確認できる

f:id:maya2250:20201109194826p:plain
management-console-nested-stack

Nested Stack 更新

Nested Stack を更新するには、親スタックを操作します。

ここでは新たに PublicSubnet3 を追加しましょう。

network.yml

diff --git a/network.yml b/network.yml
index 9a3b5a6..5b32343 100644
--- a/network.yml
+++ b/network.yml
@@ -28,3 +28,13 @@ Resources:
           - Fn::GetAZs: ""
       CidrBlock: 172.16.1.0/24
       VpcId: !Ref VPC
+
+  PublicSubnet3:
+    Type: AWS::EC2::Subnet
+    Properties:
+      AvailabilityZone:
+        Fn::Select:
+          - 2
+          - Fn::GetAZs: ""
+      CidrBlock: 172.16.2.0/24
+      VpcId: !Ref VPC

同じように package, deploy サブコマンドを実行します。

$ aws cloudformation package --template-file ./parent.yml --s3-bucket cfn-nested-stack-demo --output-template-file /tmp/response.yml
Uploading to 7051d70fce833c9fca4fa573b9cfc96d.template  833 / 833.0  (100.00%)
Successfully packaged artifacts and wrote output template to file /tmp/response.yml.
Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file /tmp/response.yml --stack-name <YOUR STACK NAME>
$ aws cloudformation deploy --template-file /tmp/response.yml --stack-name cfn-nested-stack-demo

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - cfn-nested-stack-demo

更新できました。

AWS 公式では Nested Stack を更新・削除する場合親スタックを操作することが推奨されています。 子スタックを操作するとどうなるのでしょうか。

マネコンから削除します

f:id:maya2250:20201109194849p:plain
delete nested child stack

f:id:maya2250:20201109194913p:plain
done deleted nested child stack

子スタックのみ削除され、親スタックが残った状態になりました。 また、子スタックで管理されていた VPC, Subnet も同様削除されました。

ここでは省略しますが、この状態で親スタックのみを削除することもできました。

さて、この半端に残ったスタックを更新するとどうなるのでしょうか。

今度は PublicSubnet2, PublicSubnet3 を削除してみます。

diff --git a/network.yml b/network.yml
index 5b32343..addc850 100644
--- a/network.yml
+++ b/network.yml
@@ -18,23 +18,3 @@ Resources:
           - Fn::GetAZs: ""
       CidrBlock: 172.16.0.0/24
       VpcId: !Ref VPC
-
-  PublicSubnet2:
-    Type: AWS::EC2::Subnet
-    Properties:
-      AvailabilityZone:
-        Fn::Select:
-          - 1
-          - Fn::GetAZs: ""
-      CidrBlock: 172.16.1.0/24
-      VpcId: !Ref VPC
-
-  PublicSubnet3:
-    Type: AWS::EC2::Subnet
-    Properties:
-      AvailabilityZone:
-        Fn::Select:
-          - 2
-          - Fn::GetAZs: ""
-      CidrBlock: 172.16.2.0/24
-      VpcId: !Ref VPC
$ aws cloudformation package --template-file ./parent.yml --s3-bucket cfn-nested-stack-demo --output-template-file /tmp/response.yml
Uploading to c1d0483d57bc44d918b4e545b37ee3f6.template  429 / 429.0  (100.00%)
Successfully packaged artifacts and wrote output template to file /tmp/response.yml.
Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file /tmp/response.yml --stack-name <YOUR STACK NAME>
$ aws cloudformation deploy --template-file /tmp/response.yml --stack-name cfn-nested-stack-demo

Waiting for changeset to be created..
Waiting for stack create/update to complete

Failed to create/update the stack. Run the following command
to fetch the list of events leading up to the failure
aws cloudformation describe-stack-events --stack-name cfn-nested-stack-demo

f:id:maya2250:20201109194935p:plain
failed update nested stack
f:id:maya2250:20201109194952p:plain
failed update nested stack event

スタック更新に失敗しました。

子スタックを削除した場合は Nested Stack 自体削除するしかなさそうです。 Nested Stack を用いて AWS リソースの作成・変更等を運用していくのであれば、公式推奨通りに親スタックのみを操作するのがよいでしょう

Nested Stack の使いどころ

Cross Stack Reference に比べると分割されたスタックをまとめて管理できデプロイも簡単になるので、 Nested Stack を進んで使っていこう!とも思ったのですがデメリットもあります。

  1. 更新時の影響範囲が広い

    基本的に Nested Stack 更新時は Nested 管理下の子スタックにも更新が掛かります

  2. 子スタックの Change Set が見れない

    子スタック(AWS::CloudFormation::Stack リソースタイプ)が Modify されるのはわかるのですが、 詳細までは見れません

たくさんのリソースを管理したい、CI/CD Pipeline に CloudFormation Stack 更新を乗せたい、 ということあれば、Nested Stack で管理しないことをお勧めします (個人的には変更差分の詳細が見れないのが辛い…)。

上記デメリットを許容できるのであれば Nested Stack で構築するのもありだと思います。

さいごに

今回は CloudFormation Nested Stack の作り方・デメリット等紹介しました。 package, deploy サブコマンドを使用して CloudFormation Nested Stack を作成・更新したり、 子スタックのみを削除した際の挙動を確認しました。

CloudFormation Nested Stack を使用する際には、まずは運用まで回せるのかを検証することをお勧めします。