OpenPolicyAgentでS3のバケットを作成するときのポリシーチェックをしてみた
今回はS3のバケットへのアクセス制限の設定についてポリシーチェックをしていこうと思います。
環境
- OS: CentOS 7
- Terraform : 0.12.26
- opa: Version: 0.21.0
S3のポリシーチェックで必要なもの
最終的に出来上がるものです。
EC2からVPC Endpointを通り、Access PointからS3へアクセスするようにします。 S3へのアクセス制限をする際に設定する内容は下記のものを想定しています。
ポリシーという単語をOpen Policy Agent のRegoで設定するポリシーとIAMポリシー、バケットポリシーいろんな意味で出てきてしまうため、単に「ポリシー」とあるものはOpen Policy Agentでチェックするポリシーとします。
また、今回はTerraformのtfファイルからjsonファイルへ返還する方法は特に記述せずに進めていきます。変換方法や返還後のjsonフォーマットが気になる方は下記を参考にしてみてください。
では、それぞれのポリシーの実装についてみていきましょう。
EC2に割り当てるIAM Role
EC2に割り当てられているIAMロールのIAMポリシーがS3へのアクセスを許可しているかチェックをしていきます。 まず、TerraformでEC2にIAMプロファイルを指定します。
resource "aws_instance" "sample" { ・・・ iam_instance_profile = aws_iam_instance_profile.sample_profile.name ・・・ }
そしてIAMプロファイルとIAMロール、IAMポリシーを作成します。
resource "aws_iam_instance_profile" "sample_profile" { name = "SampleEC2Role" role = aws_iam_role.sample_role.name } resource "aws_iam_role" "sample_role" { name = "SampleEC2Role" assume_role_policy = data.aws_iam_policy_document.ec2_role.json } resource "aws_iam_role_policy" "ec2_role_policy" { name = "SampleEC2RolePolicy" role = aws_iam_role.sample_role.id policy = data.aws_iam_policy_document.ec2_role_policy.json } data "aws_iam_policy_document" "ec2_role_policy" { statement { effect = "Allow" actions = [ "s3:*" ] resources = [ "*" ] } }
IAMロールに割り当てるIAMポリシーはすべてのS3に対して、すべてのアクションを許可するようにしています。 IAMポリシーの設定方法はjsonファイルとして記述することもできますが、Open Policy Agentでチェックする場合はjsonではなくHCLで記述したほうが都合がいいためHCLで記述します。
では、このIAMポリシーの設定をチェックするポリシーを見ていきましょう。
valid_ec2_iam_role { resources := ec2_role_policy_data count({resource| resources[resource]; ec2_iam_role_policy_s3_filter(resource) }) == count(resources) } ec2_iam_role_policy_s3_filter(resource) { statement := resource.expressions.statement[_] value := statement.actions.constant_value[_] re_match("s3:.*", value) statement.effect.constant_value == "Allow" }
valid_ec2_iam_role
のRuleでEC2に割り当てるIAMロールのIAMポリシーをチェックをしています。
まず、 ec2_role_policy_data
でEC2に割り当てられているIAMロールのIAMポリシーを取得しています。そして ec2_iam_role_policy_s3_filter
関数でS3へのアクセスが許可されているIAMポリシーをフィルターしています。
statement := resource.expressions.statement[_]
でIAMポリシーのステートメントをとりだし value := statement.actions.constant_value[_]
でactions の値を取得しています。
re_match("s3:.*", value)
では正規表現で "s3:.*" の正規表現に当てはまっているのかをチェックしてし、 statement.effect.constant_value == "Allow"
でステートメントが許可する設定となっているかをチェックしています。
フィルターし、得られたIAMポリシーの数とEC2に割り当てられているIAMロールのIAMポリシーの数が等しい場合、すべてのIAMポリシーが条件に当てはまっている状態となります。
ec2_role_policy_data
の実装方法は下記のようになっています。
ec2_iam_policy_data[resource] { policy := ec2_iam_role_policies[_] policy_data := policy.expressions.policy.references[_] resource := fetch_address_resource[policy_data] } ec2_iam_role_policies[resource] { role_address := ec2_role_addresses resource := data.configuration.root_module.resources[_] resource.expressions.role.references[_] == role_address[_] resource.type == "aws_iam_role_policy" } ec2_role_addresses[resource] { profiles := instances[_].expressions.iam_instance_profile.references[_] profile_resource := fetch_address_resource[profiles] resource := profile_resource.expressions.role.references[_] } fetch_address_resource[address] = resource { resource := data.configuration.root_module.resources[_] address := resource.address } instances[resource] { resource := data.configuration.root_module.resources[_] resource.type == "aws_instance" }
長くなってしまうため、細かい説明は省略します。 処理としては、EC2インスタンスの一覧を取得し、EC2に割り当てられているIAMプロファイルを取得。IAMプロファイルからIAMロールを取得し、IAMポリシーを取得する流れとなっています。
作成するS3のAccess Point
それでは作成するS3のAccess Pointに対するポリシーチェックをしていきます。 作成するS3とAccess Pointは下記のものとなります。
resource "aws_s3_bucket" "sample" { bucket = "sample-bucket" acl = "private" } resource "aws_s3_access_point" "sample-access-point" { bucket = aws_s3_bucket.sample.id name = "sample-access-point" vpc_configuration { vpc_id = aws_vpc.main.id } }
sample-bucket
バケットと Access Pointを作成するTerraformになります。Access Point には vpc_configuration
を指定し vpc_id
でVPCに対する作成を行います。
では、実際にS3のAccess PointがすべてVPCのみとなっているかチェックをしていきます。
ポリシーチェックの実装は下記のようになっています。
valid_s3_vpc_access_point { s3_access_points := s3_access_point count({resource| s3_access_points[resource]; count(resource.expressions.vpc_configuration) > 0 }) == count(s3_access_points) } s3_access_point[resource] { s3_address := create_s3_addresses[_] resource := reference_s3_resources[s3_address] resource.type == "aws_s3_access_point" } create_s3_addresses[address] { some i data.resource_changes[i].type == "aws_s3_bucket" data.resource_changes[i].change.actions[_] == "create" address := data.resource_changes[i].address } reference_s3_resources[s3_address] = resource { some i s3_address := data.configuration.root_module.resources[i].expressions.bucket.references[_] resource := data.configuration.root_module.resources[i] }
順番に見ていきましょう。
valid_s3_vpc_access_point
のRuleでS3のAccess Pointのチェックをしています。チェックするAccess Pointは s3_access_point
で取得しています。
s3_access_point内では create_s3_addresses
でTerraformで作成するS3のaddressを取得します。
取得したS3のaddressをもとに reference_s3_resources
で S3のaddressが関連付けられたリソースを取得します。
そして、S3に関連付けられたリソースで aws_s3_access_point
のものを返すようにすることでS3のAccess Pointを取得することができます。
取得したS3のAccess PointですべてのAccess Pointがvpc_configurationを所有しているかどうかをチェックすることですべてのAccess PointがVPCに制限されたものかどうかを判定しています。
S3へのアクセス制限
最後に、S3へのアクセスをAccess Pointのみ許可するようにします。
まず、Terraformで指定するバケットポリシーの内容を見ていきましょう。
resource "aws_s3_bucket" "sample" { bucket = "sample-bucket" acl = "private" } resource "aws_s3_access_point" "sample-access-point" { bucket = aws_s3_bucket.sample.id name = "main-access-point" vpc_configuration { vpc_id = aws_vpc.main.id } } data "aws_iam_policy_document" "s3-bucket-policy" { statement { effect = "Deny" actions = [ "s3:*Object" ] resources = [ "${aws_s3_bucket.sample.arn}/*", aws_s3_bucket.sample.arn ] principals { type = "AWS" identifiers = ["*"] } condition { test = "StringNotEquals" variable = "s3:DataAccessPointArn" values = [ aws_s3_access_point.sample-access-point.arn ] } } } resource "aws_s3_bucket_policy" "s3-main-policy" { bucket = aws_s3_bucket.sample.id policy = data.aws_iam_policy_document.s3-bucket-policy.json }
バケットポリシーもHCLの表記で記述していきます。
conditionの設定で sample-access-point
Access Point 以外からのアクセスに対して sample-bucket
バケットのオブジェクトへのアクションは拒否する設定となっています。
これで、S3のオブジェクトへのアクセスはAccess Pointを通してのアクセスのみに制限されました。
ではこの設定がされているのかをチェックするポリシーを見ていきます。
valid_s3_bucket_policy { policy := s3_bucket_policies[_] access_point := s3_access_point statement := policy.expressions.statement[_] statement.effect.constant_value == "Deny" re_match("s3:.*", statement.actions.constant_value[_]) statement.condition[_].test.constant_value == "StringNotEquals" statement.condition[_].variable.constant_value == "s3:DataAccessPointArn" statement.condition[_].values.references[_] == access_point[_].address } s3_bucket_policies[policy] { s3_address := create_s3_addresses resource := data.configuration.root_module.resources[_] resource.type == "aws_s3_bucket_policy" resource.expressions.bucket.references[_] == s3_address[_] policy := fetch_address_resource[resource.expressions.policy.references[_]] }
s3_access_point
と create_s3_addresses
は 作成するS3のAccess Point のところで出てきたものを使っています。
valid_s3_bucket_policy
でバケットポリシーが想定されているものかどうかをチェックしています。
対象となる、バケットポリシーは s3_bucket_policies
で取得しています。
valid_s3_bucket_policy
の中を細かく見ていきましょう。
まず、 s3_bucket_policies
と s3_access_point
でバケットポリシーとAccess Pointを取得します。
そしてバケットポリシーのステートメントを取得し、条件に当てはまるかをチェックしていきます。
これで S3のバケットポリシーのチェックができました。
まとめ
EC2に割り当てられてるIAMロールからIAMポリシーのチェックとS3のバケットポリシーのチェックをしていきました。
IAMポリシーとバケットポリシーのチェックをする際に、jsonファイルとして別ファイルなどにせずにHCLの表記にすることでOpen Policy Agent でチェックすることができます。aws_iam_policy_document
についても今回初めて知り使ってみましたが個人的な意見としてはjsonよりも使いやすかったので今後は aws_iam_policy_document
で書いていこうかと思います。
実装内容としては今回の実装ですとS3のバケットする際にすべてのバケットに対してポリシーチェックをするため、グローバルアクセスするものがある場合は処理を分ける必要があります。用途の異なるS3のバケットを構築する際のポリシーチェックについては別の記事で書いていこうかと思います。
Access Pointへアクセスできるものを制御したいとかある場合はアクセスポイントポリシーも設定することができるのでより厳密に制御することが可能となります。気になる方はぜひやってみてください。
[OpenPolicyAgent] Terraformで構築するEC2のSecurity Groupをチェックする
EC2を構築する際にSecurity Groupを設定することがあると思いますが、その際に想定通りの設定になっているのかを気を付ける必要がありますね。
想定通りの設定になっているのか、Open Policy Agentで確認することができるのでその実装方法についてみていきたいと思います。
(まだまだ、勉強中ですので、ご意見あればコメントいただけますと幸いです!)
動作環境
- OS: CentOS 7
- Terraform : 0.12.26
- opa: Version: 0.21.0
Terraformの内容
今回対象となるTerraformの内容は下記になります。
resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" tags = { Env = terraform.workspace } } resource "aws_subnet" "frontend_subnet" { vpc_id = aws_vpc.main.id cidr_block = "10.0.0.0/24" tags = { Env = terraform.workspace } } resource "aws_security_group" "http" { name = "http" vpc_id = aws_vpc.main.id ingress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } } resource "aws_security_group" "ssh" { name = "ssh" vpc_id = aws_vpc.main.id ingress { from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = ["1.2.3.4/32"] } } resource "aws_instance" "web" { ami = "ami-0717724173e4d9989" instance_type = "t3.micro" subnet_id = aws_subnet.frontend_subnet.id tags = { Env = terraform.workspace Role = "web" } vpc_security_group_ids = [ "${aws_security_group.http.id}", "${aws_security_group.ssh.id}" ] root_block_device { volume_type = "gp2" volume_size = "20" } }
webサーバとしてEC2を構築します。Security Group としてはhttp と ssh へのリクエストを許可し、 ssh については「1.2.3.4/32」のIPからのみ許可しています。
Terraformのファイルの準備ができましたので、下記のコマンドでjson形式に変換してみようと思います。
terraform plan --out tfplan.binary terraform show -json tfplan.binary > tfplan.json
jsonへ返還後の内容でEC2のSecurity Groupのチェックに必要な部分を下記に抜粋しております。
{ ・・・ "planned_values": { "root_module": { "resources": [ { "address": "aws_instance.web", "mode": "managed", "type": "aws_instance", "name": "web", "provider_name": "aws", "schema_version": 1, "values": { "ami": "ami-0717724173e4d9989", "credit_specification": [], "disable_api_termination": null, "ebs_optimized": null, "get_password_data": false, "hibernation": null, "iam_instance_profile": null, "instance_initiated_shutdown_behavior": null, "instance_type": "t3.micro", "monitoring": null, "root_block_device": [ { "delete_on_termination": true, "volume_size": 20, "volume_type": "gp2" } ], "source_dest_check": true, "tags": { "Env": "stagging", "Role": "web" }, "timeouts": null, "user_data": null, "user_data_base64": null } }, { "address": "aws_security_group.http", "mode": "managed", "type": "aws_security_group", "name": "http", "provider_name": "aws", "schema_version": 1, "values": { "description": "Managed by Terraform", "ingress": [ { "cidr_blocks": [ "0.0.0.0/0" ], "description": "", "from_port": 80, "ipv6_cidr_blocks": [], "prefix_list_ids": [], "protocol": "tcp", "security_groups": [], "self": false, "to_port": 80 } ], "name": "web", "name_prefix": null, "revoke_rules_on_delete": false, "tags": null, "timeouts": null } }, { "address": "aws_security_group.ssh", "mode": "managed", "type": "aws_security_group", "name": "ssh", "provider_name": "aws", "schema_version": 1, "values": { "description": "Managed by Terraform", "ingress": [ { "cidr_blocks": [ "1.2.3.4/32" ], "description": "", "from_port": 22, "ipv6_cidr_blocks": [], "prefix_list_ids": [], "protocol": "tcp", "security_groups": [], "self": false, "to_port": 22 } ], "name": "ssh", "name_prefix": null, "revoke_rules_on_delete": false, "tags": null, "timeouts": null } } ] } } ・・・ "configuration": { "provider_config": { ・・・ }, "root_module": { "resources": [ { "address": "aws_instance.web", "mode": "managed", "type": "aws_instance", "name": "web", "provider_config_key": "aws", "expressions": { ・・・ "vpc_security_group_ids": { "references": [ "aws_security_group.http", "aws_security_group.ssh" ] } }, "schema_version": 1 } ] } } }
configuration > root_module > resources の項目に構築するEC2とそれに関連するSecurity Groupの address 一覧が vpc_security_group_ids
内に記載されています。
planned_values > root_module > resources の項目にはリソースの値が入っています。
ですので、EC2に関連するSecurity Groupの address を取得し、その address をもとにSecurity Groupにどのような設定がされているのかをチェックするように実装していきます。
それぞれの内容がどういうものなのか気になる方はTerraformの公式にフォーマットの情報が記載していますので、そちらをご確認ください。
ポリシーチェック
どのようにEC2に関連しているSecurity Groupのチェックをするのかを軽く述べましたので、実装部分を見ていきたいと思います。 まず、Security Groupの設定として下記の内容をチェックしていきます。
- SSHのリクエストは「1.2.3.4/32」のアドレスからのみ許可する ※1.2.3.4のアドレスは架空のものとなります。
- HTTP、HTTPSのリクエストを許可する
- 上記内容以外のSecurity Groupの設定はエラーとする
EC2のSecurity Group
初めにEC2に関連付けられているSecurity Groupを取得していきます。
web_instances[instance] { instance := data.planned_values.root_module.resources[_] instance.type == "aws_instance" instance.values.tags.Role == "web" } web_instance_security_groups[resource] { instances := web_instances resource := data.planned_values.root_module.resources[_] data.configuration.root_module.resources[_].address == instances[_].address resource.address == data.configuration.root_module.resources[_].expressions.vpc_security_group_ids.references[_] resource.type == "aws_security_group" }
web_instances の RuleでTerraformで構築するEC2で「Role」タグにweb と設定されているEC2を取得します。 そして下記の部分でEC2のaddressと等しい configuration 内の リソース かどうかを比較しています。同じaddress の場合、そのリソースの vpc_security_group_ids に定義されているSecurity Group の addressと等しい リソースかどうかを比較しています。
data.configuration.root_module.resources[_].address == instances[_].address resource.address == data.configuration.root_module.resources[_].expressions.vpc_security_group_ids.references[_]
これで、EC2に関連付けられたSecurity Groupが取得することができました。 では実際にSecurity Groupのチェックを行っていきます。
Security Groupのポリシーチェック
EC2に関連するSecurity Group が取得できたので次はその設定がポリシー通りになっているのかをチェックします。 全体の実装内容は下記のようになります。
permit_ssh_ips = ["1.2.3.4/32"] default valid_web_instance_security_group = false valid_web_instance_security_group { valid_ssh_instance_security_group valid_http_instance_security_group valid_other_instance_security_group } valid_other_instance_security_group { count(web_instance_security_groups) == count(http_security_groups) + count(https_security_groups) + count(ssh_security_groups) } default valid_http_instance_security_group = false valid_http_instance_security_group { count(http_security_groups) > 0 } valid_http_instance_security_group { count(https_security_groups) > 0 } default valid_ssh_instance_security_group = false valid_ssh_instance_security_group { security_groups := ssh_security_groups count({security_group| security_groups[security_group]; security_group.values.ingress[_].cidr_blocks == permit_ssh_ips }) == count(security_groups) } ssh_security_groups[security_group] { security_groups := web_instance_security_groups security_group := security_groups[_] security_group.values.ingress[_].from_port == 22 security_group.values.ingress[_].to_port == 22 } http_security_groups[security_group] { security_groups := web_instance_security_groups security_group := security_groups[_] security_group.values.ingress[_].from_port == 80 security_group.values.ingress[_].to_port == 80 } https_security_groups[security_group] { security_groups := web_instance_security_groups security_group := security_groups[_] security_group.values.ingress[_].from_port == 443 security_group.values.ingress[_].to_port == 443 }
valid_web_instance_security_group
の RuleでSecurity Groupのポリシーをチェックする各Ruleを読んでいっています。
上から順にみていきたいと思います。
まず、 valid_ssh_instance_security_group
のRuleを見ていきましょう。
SSHのリクエストは「1.2.3.4/32」のアドレスからのみ許可する
valid_ssh_instance_security_group の Ruleでsshに関するSecurity Groupのポリシーをチェックしています。関連する実装部分は下記になります。
permit_ssh_ips = ["1.2.3.4/32"] default valid_ssh_instance_security_group = false valid_ssh_instance_security_group { security_groups := ssh_security_groups count({security_group| security_groups[security_group]; security_group.values.ingress[_].cidr_blocks == permit_ssh_ips }) == count(security_groups) } ssh_security_groups[security_group] { security_groups := web_instance_security_groups security_group := security_groups[_] security_group.values.ingress[_].from_port == 22 security_group.values.ingress[_].to_port == 22 }
valid_ssh_instance_security_group ではsshのSecurity Groupのポリシーチェックをしています。 最初にssh_security_groups の RuleでEC2に関連付けられたSecurity Groupでポートが22番(ssh)のSecurity Groupのみ取得しています。
そして、下記で接続ものとIPアドレス帯もチェック項目となります。
count({security_group| security_groups[security_group]; security_group.values.ingress[_].cidr_blocks == permit_ssh_ips }) == count(security_groups)
permit_ssh_ips には許可するIPアドレスの配列が記載されており、別の値が入っていた場合、count(security_groups)
の数より少なくなってしまうため、falseとなる仕組みになります。
これで、EC2に関連するSecurity Groupでsshに関するポリシーチェックは完了しました。
次にhttp(https)のSecurity Groupについてみていきます。
HTTP、HTTPSのリクエストを許可する
httpとhttpsでは両方設定されている場合もありますが、どちらか片方のみしか設定されていない場合もあります。ですのでhttp or httpsの実装がある、というチェックを行う必要があります。では実装を見ていきましょう。
default valid_http_instance_security_group = false valid_http_instance_security_group { count(http_security_groups) > 0 } valid_http_instance_security_group { count(https_security_groups) > 0 } http_security_groups[security_group] { security_groups := web_instance_security_groups security_group := security_groups[_] security_group.values.ingress[_].from_port == 80 security_group.values.ingress[_].to_port == 80 } https_security_groups[security_group] { security_groups := web_instance_security_groups security_group := security_groups[_] security_group.values.ingress[_].from_port == 443 security_group.values.ingress[_].to_port == 443 }
httpとhttpsで同名のRuleとして定義しています。これは valid_http_instance_security_group の Rule でどちらかのRule が true になったら true になります。ですので、http か https のSecurity Group がある場合、 valid_http_instance_security_group は true となります。
上記内容以外のSecurity Groupの設定はエラーとする
最後に、http(https)とssh 以外のSecurity Group が設定されていた場合、falseになるように Rule を追加します。
valid_other_instance_security_group { count(web_instance_security_groups) == count(http_security_groups) + count(https_security_groups) + count(ssh_security_groups) }
count(web_instance_security_groups)
で EC2に関連しているSecurity Groupの数を取得し、 count(http_security_groups) + count(https_security_groups) + count(ssh_security_groups)
で http(https)とsshのSecurity Group の数を取得し同数になっているかを比較しています。同数になっている場合は別のSecurity Group が設定されていないため true となります。
これで、不要なSecurity Group が設定されていないかチェックすることができました。
まとめ
EC2に関連するSecurity Groupの設定からポリシーをチェックする方法を見てみました。リソース間の関連付けについては configuration 内のリソースに定義されており、そこから planned_values の値のポリシーをチェックしています。Regoの表記で configuration のリソース内の情報から planned_values 内のリソースへの関連付けを記述することができるので、割とサクッと実装することができました。 各 Security Groupを取得しているとことですが、愚直に http, https, sshを別々に定義していますが、ここは下記のようにまとめることができます。
security_groups[port] = security_group { some port ports[port] security_groups := web_instance_security_groups security_group := security_groups[_] security_group.values.ingress[_].from_port == port security_group.values.ingress[_].to_port == port } > security_groups[80]
こうすることで、 security_groups の Ruleに渡したポート番号のSecurity Groupの一覧を取得することができます。 Regoの表記もまだまだ触ったことがないものなどありますので、別の機会に見ていきたいと思います。 また、今回はwebサーバに直接リクエストが行くようになっていますが、ALBを用いた場合のポリシーチェックなどをもしていきたいと思います。
[OpenPolicyAgent] TerraformでEC2を構築する際のポリシーをチェックする
Open Policy Agentを使うことでTerraformでEC2を構築する際にインスタンス設定が想定されているものなのかチェックができるため実装してみました。
やってみた内容について簡単にまとめていきたいと思います。
実装方法などはまだまだ勉強中ですのであくまでも参考程度に見ていただければと思います。(よい実装方法などあれば教えてくれると嬉しいです!)
動作環境
OS: CentOS 7 Terraform : 0.12.26 opa: Version: 0.21.0
Open Policy AgentでTerraformのポリシーチェック
まずはOpen Policy AgentでTerraformのポリシーをチェックする方法を見ていきます。
Terraformは .tf
という拡張子でHCLで記述していきます。このままですとOpen Policy Agentでポリシーチェックができませんので、json形式に変換する必要があります。
変換方法としては下記のようにすることでjsonファイルに変換することができます。
terraform plan --out tfplan.binary terraform show -json tfplan.binary > tfplan.json
terraform plan --out ftplan.binary
でTerraformのプラン結果をファイルに出力することができます。
そして terraform show -json tfplan.binary
でプランで出力した内容をjson形式としてファイルに出力することができます。
これでOpen Policy Agentでポリシーチェックをする対象の準備ができました。
ポリシーチェックするTerraformのファイル
では、早速ポリシーチェックをするTerraformを見ていこうと思います。
resource "aws_instance" "web" { ami = "ami-0717724173e4d9989" instance_type = "t3.micro" tags = { Env = terraform.workspace } root_block_device { volume_type = "gp2" volume_size = "20" } }
今回は上記のtfファイルの内容が下記のポリシー通りになっているのかを見ていきたいと思います。
- タグに「Env」が存在しており、stagging or production のどちらかが設定されているか
- ステージング環境の場合、インスタンスタイプが t3.micro か
- ルートボリュームでサイズが20以上、100以下になっているか、typeがgp2になっているか
前提として Env タグに設定されている値は環境を表しており、staggingの場合はステージング環境、productionの場合はプロダクション環境となります。 また、Env タグに workspaceの値を入れるようにしているため、EC2を複数構築ときでもステージング環境とプロダクション環境の二つの環境に同時にデプロイすることはできない想定です。
このtfファイルをjson形式に変換した時の内容は下記のようになります。
{ ・・・ "resource_changes": [ { "address": "aws_instance.web", "mode": "managed", "type": "aws_instance", "name": "web", "provider_name": "aws", "change": { "actions": [ "create" ], "before": null, "after": { "ami": "ami-0717724173e4d9989", "credit_specification": [], "disable_api_termination": null, "ebs_optimized": null, "get_password_data": false, "hibernation": null, "iam_instance_profile": null, "instance_initiated_shutdown_behavior": null, "instance_type": "t3.micro", "monitoring": null, "root_block_device": [ { "delete_on_termination": true, "volume_size": 20, "volume_type": "gp2" } ], "source_dest_check": true, "tags": { "Env": "stagging" }, "timeouts": null, "user_data": null, "user_data_base64": null }, ・・・
resource_changes
に構築するEC2の情報が記述されています。構築時のポリシーチェックになるのでここの要素に対してポリシーを作成していきます。
EC2のポリシーをチェックする
ポリシーは下記のように実装しました。
package terraform.aws import input as tfplan stagging_instance_type = ["t3.micro"] production_instance_type = ["m5.large"] env_tags = ["stagging", "production"] default allow = false allow { valid_instance_tag valid_instance_root_block_device valid_environment_instance_type } valid_instance_root_block_device { instances := aws_instances count({instance| instances[instance]; valid_instance_root_block_device_filter(instance) }) == count(instances) } valid_instance_root_block_device_filter(instance) { instance.change.after.root_block_device[_].volume_size >= 20 instance.change.after.root_block_device[_].volume_size <= 100 instance.change.after.root_block_device[_].volume_type == "gp2" } valid_instance_tag { instances := aws_instances count({instance| instances[instance]; instance_tag_filter(instance) }) == count(instances) } instance_tag_filter(instance) { instance.change.after.tags.Env == env_tags[_] } default valid_environment_instance_type = false valid_environment_instance_type = true { valid_stagging_instance_type } valid_environment_instance_type = true { valid_production_instance_type } aws_instances[instance] { instance := tfplan.resource_changes[_] instance.type == "aws_instance" } stagging_instances[instance] { instances := aws_instances instance := instances[_] instance.change.after.tags.Env == "stagging" } valid_stagging_instance_type { instances := stagging_instances count({instance| instances[instance]; stagging_instance_type_filter (instance) }) == count(instances) } stagging_instance_type_filter (instance) { instance.change.actions[_] == "create" instance.change.after.instance_type == stagging_instance_type[_] } production_instances[instance] { instances := aws_instances instance := instances[_] instance.change.after.tags.Env == "production" } valid_production_instance_type { instances := production_instances count({instance| instances[instance]; production_instance_type_filter(instance) }) == count(instances) } production_instance_type_filter(instance) { instance.change.actions[_] == "create" instance.change.after.instance_type == production_instance_type[_] }
ここでは
- タグに「Env」が存在しており、stagging or production のどちらかが設定されているか
- ステージング環境の場合、インスタンスタイプが t3.micro か
- ルートボリュームでサイズが20以上、100以下になっているか、typeがgp2になっているか
をチェックしています。
では、 どのようにしてチェックしているか
を順を追ってみていきます。
allow
allowというRuleの中で複数のRuleを記述しています。allowの中に記述しているRule内で細かくポリシーのチェックを行い、すべてのRuleがtrueになった場合にallowもtrueになります。
default allow = false allow { valid_instance_tag valid_instance_root_block_device valid_environment_instance }
タグに「Env」が存在しており、stagging or production のどちらかが設定されているか
EC2に「Env」タグが存在しているか、「Env」タグが stagging か production かどうかは valid_instance_tag でチェックしています。 実装している部分は下記のところとなります。
valid_instance_tag { instances := aws_instances count({instance| instances[instance]; instance_tag_filter(instance) }) == count(instances) } instance_tag_filter(instance) { instance.change.after.tags.Env == env_tags[_] }
instances := aws_instances
ですがこちらは構築対象となるインスタンス一覧を取得しています。
aws_instances
のRuleの内容ですが下記のようになっています。
aws_instances[instance] { instance := tfplan.resource_changes[_] instance.type == "aws_instance" }
簡単にまとめると resource_changes
の一覧で type
が aws_instance
のリソースを取得するため、構築するEC2の一覧を取得することができます。
詳しくは公式の内容を見ていただけるとわかるかと思います。
では本題の タグに「Env」が存在しており、stagging or production のどちらかが設定されているか
をチェックしている Rule を見てみたいと思います。
内容を一部抜粋して下記に記載してあります。
env_tags = ["stagging", "production"] instances := aws_instances count({instance| instances[instance]; instance_tag_filter(instance) }) == count(instances) instance_tag_filter(instance) { instance.change.after.tags.Env == env_tags[_] }
ここで重要になるのが instance_tag_filter 関数の内容になります。 instance_tag_filter関数でtagsにEnvがあり、Envの値が env_tags 内のものと一致しているインスタンス一覧を返す処理をしています。
そして count({instance| instances[instance]; instance_tag_filter(instance) })
の処理ですが instance_tag_filter関数から取得したインスタンス一覧の数を取得し、count(instances)
で構築するEC2インスタンスの数を取得しています。
つまり instance_tag_filter関数から取得したインスタンス一覧の数と構築するEC2インスタンス数が同数かをチェックしています。
こうすることで、すべてのEC2インスタンスがタグに「Env」があり、 stagging or productionの値が設定されているかをチェックすることができます。 この書き方は公式に記載がありますので、詳しく知りたい方は公式を見てみてください。
これで、EC2に「Env」タグがあり、stagging or production の値が設定されているかのチェックができました。
ステージング環境の場合、インスタンスタイプが t3.micro か
次にステージング環境の場合、インスタンスタイプが t3.micro かのチェック方法を見ていきたいと思います。 っとその前に、「Env」タグの値が「stagging」なのか「production」なのかで分岐する必要があります。ただ、Ruleとして下記のように記載してしまうと誤った挙動となります。
valid_environment_instance_type { valid_stagging_instance_type valid_production_instance_type }
valid_stagging_instance_type が staggingの場合のインスタンスタイプチェック、valid_production_instance_type が production の場合のインスタンスタイプチェックとなります。
こうしてしまうと、両方のRuleがtrueになったときにしか valid_environment_instance_type
の Ruleがtrueしません。
前提としてステージング環境とプロダクション環境を同時にデプロイすることはないので、仮にステージング環境にデプロイしようとすると Envタグに production が設定されているものがなく、valid_production_instance_type の結果が undefined となり、valid_environment_instance_type が falseになります。
理想はstaggingとproductionが同時に指定されることはないため valid_stagging_instance_type と valid_production_instance_type でどちらかがtrueになった場合、valid_environment_instance_type がtrueになるようにしたいです。 そのためには下記のように設定する必要があります。
default valid_environment_instance_type = false # ステージング環境 valid_environment_instance_type = true { valid_stagging_instance_type } # プロダクション環境 valid_environment_instance_type = true { valid_production_instance_type }
こうすることで、どちらかのRuleがtrueになれば valid_environment_instance_type が true になります。(ほかにいい方法があれば教えていただけると嬉しいです!)
この書き方は公式に載っていますので、詳しくはこちらを参考にしてください。
では、valid_stagging_instance_type のRuleの内容を見ていきたいと思います。
stagging_instance_type = ["t3.micro"] valid_stagging_instance_type { instances := stagging_instances count({instance| instances[instance]; stagging_instance_type_filter(instance) }) == count(instances) } stagging_instances[instance] { instances := aws_instances instance := instances[_] instance.change.after.tags.Env == "stagging" } stagging_instance_type_filter(instance) { instance.change.actions[_] == "create" instance.change.after.instance_type == stagging_instance_type[_] }
ここでもタグのポリシーチェックのときと同様に条件に当てはまるもの(stagging_instance_type_filter 関数で取得したインスタンス一覧)が作成するEC2インスタンス数と同数になっているかをチェックしています。
これをもう少しかみ砕いて整理していきたいと思います。
まず、 stagging_instances でEnvタグにstaggingが設定されているインスタンスの一覧を取得します。
そして、stagging_instance_type_filter 関数で条件に当てはまるインスタンス一覧(ここではインスタンスタイプ)を取得します。関数の中で、
instance.change.after.instance_type == stagging_instance_type[_]
とありますが、ステージング環境に構築できるインスタンスタイプかどうかを判定しています。
stagging_instance_type
には t3.micro
のみ設定しているため、ステージング環境では t3.micro のみ許可します(もちろん、複数指定することも可能です)。
こうすることで、環境ごとでインスタンスタイプが想定されているものなのかをチェックすることができました。
ルートボリュームでサイズが20以上、100以下になっているか、typeがgp2になっているか
最後のチェック項目として、 root_block_device でサイズが 20以上、100以下になっているか、typeがgp2になっているか をチェックしていきます。
valid_instance_root_block_device { instances := aws_instances count({instance| instances[instance]; valid_instance_root_block_device_filter(instance) }) == count(instances) } valid_instance_root_block_device_filter(instance) { instance.change.after.root_block_device[_].volume_size >= 20 instance.change.after.root_block_device[_].volume_size <= 100 instance.change.after.root_block_device[_].volume_type == "gp2" }
ここまで記事を読んでいただいた方には上記内容で伝わるかと思いますが、、簡単に説明していきたいと思います。
valid_instance_root_block_device の Rule内 構築するインスタンス一覧と valid_instance_root_block_device_filter 関数から取得したインスタンス一覧が同数になっているかをチェックしています。
valid_instance_root_block_device_filter 関数では root_block_device の値で volume_size と volume_type が想定されているものかどうかをチェックし、該当するインスタンス一覧を取得します。 これで、 root_block_device 内の値がポリシー通りになっているかをチェックすることができました。
まとめ
以上がTerraformで構築するEC2インスタンスのポリシーチェックになります。ポリシーを機械的にチェックできるので設定ミスや不正なもののデプロイを防ぐことができますし、レビューの負担も減らすことができると思います。 また、今回は構築(actionsがcreate)時のみインスタンスタイプのチェックしていました。同様にアクションごとでポリシーのチェックを利用することで更新や削除についてもチェックすることができるかと思いますので、別の機会に試してみたいと思います。 S3とかのポリシーについても実装してみたいですね。
Open Policy Agentへ入門
CNCFのプロジェクトにOpen Policy Agentっというものがあります。 Open Policy Agentはポリシーエンジンとして動作し、指定したポリシーに準じているかどうかをチェックしてくれます。 そんなOpen Policy Agentについて軽く触ってみたのでどういった機能があるのか、気になった部分を中心に書いていきたいと思います。
目次
- Open Policy Agent
- Regoの特徴
- Regoの記述
- Regoのテスト
- 終わりに
1. Open Policy Agentについて
上記で簡単に説明しましたが、Open Policy Agentとはポリシーエンジンとして動作し、ポリシーのチェックを行います。
利用方法としてはコマンドラインで利用したり、デーモンで動かすパターンとライブラリとして利用するパターンがあります。
KubernetesやEnvoy、Kafkaと連携したり、Terraformのプラン結果をチェックできたりと、いろんな用途で扱うことができそうです。
基本的にはJSONデータであればポリシーチェックを行える感じですかね。
Terraformのポリシーチェックに関しても plan
した結果を JSON形式に出力してチェックをしてますし、Kubernetesに関してもマニュフェストファイルをJSON形式に変換してチェックをしているようです。
次は、Open Policy Agentでポリシーを記述する「Rego」についてみて行きたいと思います。
2. Regoの特徴
Open Policy Agentではポリシーを定義するため、「Rego」っという言語で記述していきます。 宣言的にポリシーを記述でき、強力な表現方法を持っていきます。 特に配列に対する操作が特殊な部分があり理解するのに少々手間取ってしまいました。。。 例えば下記のような記述があったとします。
example.rego
package example default allow = false allow { has_environment["prod"] } has_environment [name] { sites = [ { "name": "prod" }, { "name": "dev" }, ] name := sites[_].name }
まずは詳細は理解せず雰囲気で見てもらいたいのですが、 allow
から has_environment
を呼び、引数として prod
っという文字列を渡しています。
これをコマンドラインから実行すると下記のような結果が返ってきます。
下記のコマンドでは上記のRegoのファイルを引数として渡し、 allow
を実行しています。
$ ./opa eval --data example.rego 'data.example.allow' { "result": [ { "expressions": [ { "value": true, "text": "data.example.allow", "location": { "row": 1, "col": 1 } } ] } ] }
"value": true,
が返ってきていますね。これはRegoの評価した結果が true
になった証拠です。
ためしに、 has_environment
の引数を stg
にしてみます。すると結果が false
になってしまいます。
これは、 sites
の要素で name
属性の値を網羅的に引数の値(name)と比較して同じものがあるかどうかをチェックしてるっという感じです。
また、Regoでは先ほどの has_environment
や allow
はルールをまとめるルールとして定義しています。
has_environment
では引数の値が sites
の要素で name
属性の値と比較し評価しています。allow
では has_environment
の結果を評価しています。
そして、allow
ではすべての評価対象が true
のときのみ true
を返します。
Regoではこうしてルールを定義し、評価していくことでポリシーに準じているかどうかをチェックしています。 また、こうして宣言的に記述したほうがポリシーの評価を簡単に行えます。
どちらかというとAlloyとか形式手法の言語に近いんですかね。
3. Regoの記述
基本的な記述方法
ここからREPLを使ってRegoの記述について触れていこうかと思います。 一般的な記述として以下のように値を扱えます。
x := 42 rect := {"width": 2, "height": 4}
ここでは x
を42にとし、 rect
を {"width": 2, "height": 4}
として扱えます。
> x == 42 true > x == 43 false
等価の評価は ==
で行うことができます。
また、下記のように記述することでルールを作成することができます。
> t { x := 42; y := 41; x > y } > t true
仮にルールを評価したときに false
になった時は下記のようになります。
> t2 { x := 42; y := 41; x < y } > t2 undefined
Regoではルールの中の評価する順序について特殊な扱いをすることができ、下記のように記述することが可能となります。
> s { x > y; y = 41; x = 42 }
ここでは :=
ではなく =
を扱っています。こうすることで記述する順番を気にすることなくルールを定義することも可能です。
次に配列を見ていきたいと思います。 上記でも記載しましたが、配列は下記のように記述することが可能です。
> sites = [{"name": "prod"}, {"name": "smoke1"}, {"name": "dev"}] > r { sites[_].name == "prod" } > r true
sites[_].name
で sites
の要素を網羅的に name
属性の値をチェックし、prod
がないかチェックしています。
[_]
が配列の中を網羅的に参照するので下記のようなに記述した場合、sites
の要素の name
属性のみが返ってきます。
> q[name] { name := sites[_].name } > q [ "prod", "smoke1", "dev" ]
ここで、 q
のルールを包括している p
のルールを作成して実行してみたいと思います。
> p { q["prod"] } > p true
true
が返ってきましたね。ここで sites
要素の name
属性に存在しない値を入れてみたいと思います。
> w { q["smoke2"] } > w undefined
true
ではなくなりましたね。ちゃんと評価できているのが確認できます。
他にも配列ではイテレート処理に関して下記のように記述することも可能です。
> some i; sites[i] +---+-------------------+ | i | sites[i] | +---+-------------------+ | 0 | {"name":"prod"} | | 1 | {"name":"smoke1"} | | 2 | {"name":"dev"} | +---+-------------------+
見てわかるように、ほかの言語(PHPとか)と同じように変数っぽく値を扱えたり、スカラー値や配列なども表現することができます。
違いとしては何度も言っていますが、宣言的な記述で手続きではないっというところですかね。
4. Regoのテスト
もう一つ、Open Policy Agentで気になった部分として先ほどのRegoに対してテストを書くことができます。 まずはサンプルを見てみたいと思います。
example.rego
package authz allow { input.path == ["users"] input.method == "POST" } allow { some profile_id input.path = ["users", profile_id] input.method == "GET" profile_id == input.user_id }
そして、先ほどのRegoをテストするコードを記述していきます。
example_test.rego
package authz test_post_allowed { allow with input as {"path": ["users"], "method": "POST"} } test_get_anonymous_denied { not allow with input as {"path": ["users"], "method": "GET"} } test_get_user_allowed { allow with input as {"path": ["users", "bob"], "method": "GET", "user_id": "bob"} } test_get_another_user_denied { not allow with input as {"path": ["users", "bob"], "method": "GET", "user_id": "alice"} }
ではこれを実行してみます。
$ ./opa test . -v data.authz.test_post_allowed: PASS (785.051µs) data.authz.test_get_anonymous_denied: PASS (443.895µs) data.authz.test_get_user_allowed: PASS (526.708µs) data.authz.test_get_another_user_denied: PASS (362.388µs) -------------------------------------------------------------------------------- PASS: 4/4
おぉーテストできましたね(サンプルをコピペしただけですが)。
これは、ルールのプレフィックスが test_
で始まっているものを実行していっています。
テストは実行した結果が true
になる場合のみ PASS
となり、それ以外は FAIL
、もしくは ERROR
となります(そもそもランタイムエラーとか)。
サンプルとして下記のテストを実行してみます。
pass_fail_error_test.rego
package example # This test will pass. test_ok { true } # This test will fail. test_failure { 1 == 2 } # This test will error. test_error { 1 / 0 }
$ ./opa test pass_fail_error_test.rego data.example.test_failure: FAIL (414.31µs) data.example.test_error: ERROR (400.987µs) pass_fail_error_test.rego:15: eval_builtin_error: div: divide by zero -------------------------------------------------------------------------------- PASS: 1/3 FAIL: 1/3 ERROR: 1/3
test_failure
で結果が FAIL
となり、 test_error
ではランタイムエラーとなり ERROR
となりましたね。
テストを書けることでポリシーの品質の担保を行えるので、非常にありがたいですね。
4. 終わりに
Open Policy Agentについて軽く触ってみて、Regoの記述の仕方など興味深いものがありました。 ポリシーを評価するにあたって配列の扱いについてかなり表現の仕方が柔軟でほかの言語と書き方がだいぶ違うので戸惑うかと思いますが、慣れてくればすごく扱いやすいものになるんじゃないかと思います。
テストについてもポリシー自体の品質を担保してくれるので、とてもありがたい機能ですね。
次はOpen Policy Agentをアプリケーションの中で使ってみるか、Terraformの出力結果とかで使ってみたいと思います。
Prometheus PushgatewayにGolangからメトリクスを登録する
PrometheusではExporterに対してメトリクスを取得するため、HTTPリクエストを行いますが、これではバッチ処理などの一時的に起動するものなどのメトリクスを収集することができません。
そのため、PrometheusではPushgatewayというものが用意されています。これはバッチ処理などのメトリクスを保存し、PrometheusからPushgatewayに対して保存されているメトリクスを取得する仕組みになっています。
Pushgatewayにメトリクスを追加するためのライブラリがGolangにあったので、今回はこれを使ってメトリクスを収集したいと思います。
目次
- 動作環境
- PrometheusとPushgatewayの起動
- バッチ処理の作成
- 終わりに
1. 動作環境
- Golang: 1.13.1
- Prometheus: 2.15.1
2. PrometheusとPushgatewayの起動
まずは、PrometheusとPushgatewayを起動させます。PushgatewayはDocker Imageが用意されているようなので、それを使って起動させます。
$ docker pull prom/pushgateway $ docker run -d -p 9091:9091 prom/pushgateway
これだけで完了です。 PushgatewayもExporterと同様にHTTPリクエストを受けれる口が用意されています。下記が確認用としてcurlでリクエストした結果になります。
$ curl http://localhost:9091/metrics # HELP go_gc_duration_seconds A summary of the GC invocation durations. # TYPE go_gc_duration_seconds summary go_gc_duration_seconds{quantile="0"} 9.943e-06 go_gc_duration_seconds{quantile="0.25"} 3.0521e-05 go_gc_duration_seconds{quantile="0.5"} 4.2184e-05 go_gc_duration_seconds{quantile="0.75"} 0.000109096 go_gc_duration_seconds{quantile="1"} 0.000429447 go_gc_duration_seconds_sum 0.003177886 go_gc_duration_seconds_count 38 # HELP go_goroutines Number of goroutines that currently exist. # TYPE go_goroutines gauge go_goroutines 12 ・・・
また、curlからメトリクスの追加もできます。
$ echo "some_metric 3.14" | curl --data-binary @- http://192.168.33.141:9091/metrics/job/some_job $ curl http://localhost:9091/metrics ・・・ pushgateway_http_requests_total{code="200",handler="push",method="post"} 3 pushgateway_http_requests_total{code="200",handler="push",method="put"} 2 pushgateway_http_requests_total{code="200",handler="status",method="get"} 1 # TYPE some_metric untyped some_metric{instance="",job="some_job"} 3.14
無事に追加できるところまで確認できましたね。
では次にPrometheusの設定をしていきます。 Prometheusでは通常の設定と同様、scrapeに設定を追加していきます。
- job_name: 'push gateway' metrics_path: /metrics static_configs: - targets: ['192.168.33.141:9091']
192.168.33.141
はPushgatewayが起動してるサーバのIPになります。
これだけで準備完了となります。
3. バッチ処理の作成
次にバッチ処理をGolangで書いていきたいと思います。
まずはサンプルとして Counter
のメトリクスを作成し、Pushgatewayに送信したいと思います。
package main import ( "fmt" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/push" ) func main() { sample := prometheus.NewCounter(prometheus.CounterOpts{ Name: "myapp_sample_counter", Help: "myapp sample counter", }) // 値をインクリメント sample.Inc() if err := push.New("http://192.168.33.141:9091", "my_job"). Collector(sample). Push(); err != nil { fmt.Printf(err.Error()) } }
では、これを実行し、Pushgatewayでメトリクスを確認したいと思います。
$ curl http://localhost:9091/metrics ・・・ # HELP myapp_sample_counter myapp sample counter # TYPE myapp_sample_counter counter myapp_sample_counter{instance="",job="my_job"} 1
無事に登録されていますね。 バッチ処理のメトリクスっぽく起動時間を登録したいと思います。
サンプルなので途中で10秒停止して時間を計測したいと思います。
execute := prometheus.NewSummary(prometheus.SummaryOpts{ Name: "my_batch_job_execution_time", Help: "execution time", }) // 時間の計測 start := time.Now() time.Sleep(time.Second * 10) end := time.Now() // 実行時間を登録 execute.Observe((end.Sub(start)).Seconds()) if err := push.New("http://192.168.33.141:9091", "my_job"). Collector(execute). Push(); err != nil { fmt.Printf(err.Error()) }
$ curl http://localhost:9091/metrics ・・・ # HELP my_batch_job_execution_time execution time # TYPE my_batch_job_execution_time summary my_batch_job_execution_time_sum{instance="",job="my_job"} 10.000940275 my_batch_job_execution_time_count{instance="",job="my_job"} 1
登録できましたね。めでたし。
4. 終わりに
簡単ではありますが、Pushgatewayでバッチ処理のメトリクス収集までやってみました。
カスタムメトリクスの収集方法を覚えれば、Exporterでも役立つのでもっと使い方を調べていきたいと思います。
PrometheusでPhoenixを監視する
以前PrometheusでElixir Plugからメトリクスの収集をできるよう設定をしました。 今回はPhoenixで設定をしていきたいと思います。
ちなみに、下記のものがPlug単体のときに設定したものになります。
目次
- 環境情報
- Phoenixのメトリクス収集の設定
- カスタムメトリクスに入門する
- 終わりに
1. 環境情報
2. Phoenixのメトリクス収集の設定
諸々のインストール
mix.exs
に prometheus_phoenix
と prometheus_plugs
を追加します
・・・ {:jason, "~> 1.0"}, {:plug_cowboy, "~> 2.0"}, {:prometheus_phoenix, "~> 1.3.0"}, {:prometheus_plugs, "~> 1.1.5"} ] end
追加したら deps.get
でインストールしていきます。
$ mix deps.get ・・・ New: accept 0.3.5 prometheus 4.4.1 prometheus_ex 3.0.5 prometheus_phoenix 1.3.0 prometheus_plugs 1.1.5 * Getting prometheus_phoenix (Hex package) * Getting prometheus_plugs (Hex package) * Getting accept (Hex package) * Getting prometheus_ex (Hex package) * Getting prometheus (Hex package)
必要なもののインストールが完了したので、ExporterとPhoenixのメトリクス収集のための設定をしていきたいと思います。
Exporterとメトリクス収集の設定
まずは、Exporter用のモジュールの作成をしていきます。
defmodule SamplePhoenix.PrometheusExporter do use Prometheus.PlugExporter end
これはPlugでの設定と同様のものになります。
次に、Phoenixのメトリクス収集のためのモジュールを作成していきます。
defmodule SamplePhoenix.Instrumenter do use Prometheus.PhoenixInstrumenter end
use Prometheus.PhoenixInstrumenter
を記述するだけという、簡単なものになります。
ExporterとPhoenixのメトリクス収集用のモジュールが作成できたので application.ex
にセットアップ処理を追加します。
def start(_type, _args) do ・・・ SamplePhoenix.Instrumenter.setup() SamplePhoenix.PrometheusExporter.setup() opts = [strategy: :one_for_one, name: SamplePhoenix.Supervisor] Supervisor.start_link(children, opts)
それぞれのモジュールで setup
メソッドを呼べば完了となります。
次にExporter用のPlugの設定をしていきます。
Phoenixではよく router.ex
にPlugの追加をすると思いますが、PrometheusのPlugの追加は endpoint.ex
に追加します。
・・・ plug SamplePhoenix.PrometheusExporter plug SamplePhoenixWeb.Router end
これで一通り準備完了です。
一度、適当にアクセス(トップページとか)してから /metrics
にアクセスし、メトリクスを確認してみましょう。
phoenix_controller_render_duration_microseconds_bucket{format="html",template="index.html",view="Elixir.SamplePhoenixWeb.PageView",le="10"} 0 phoenix_controller_render_duration_microseconds_bucket{format="html",template="index.html",view="Elixir.SamplePhoenixWeb.PageView",le="25"} 0 phoenix_controller_render_duration_microseconds_bucket{format="html",template="index.html",view="Elixir.SamplePhoenixWeb.PageView",le="50"} 0 phoenix_controller_render_duration_microseconds_bucket{format="html",template="index.html",view="Elixir.SamplePhoenixWeb.PageView",le="100"} 8 ・・・ phoenix_channel_join_duration_microseconds_bucket{channel="Phoenix.LiveReloader.Channel",topic="phoenix:live_reload",transport="websocket",le="10000000"} 7 phoenix_channel_join_duration_microseconds_bucket{channel="Phoenix.LiveReloader.Channel",topic="phoenix:live_reload",transport="websocket",le="+Inf"} 7 phoenix_channel_join_duration_microseconds_count{channel="Phoenix.LiveReloader.Channel",topic="phoenix:live_reload",transport="websocket"} 7 phoenix_channel_join_duration_microseconds_sum{channel="Phoenix.LiveReloader.Channel",topic="phoenix:live_reload",transport="websocket"} 748.044 ・・・ phoenix_controller_call_duration_microseconds_bucket{action="index",controller="SamplePhoenixWeb.PageController",le="+Inf"} 15 phoenix_controller_call_duration_microseconds_count{action="index",controller="SamplePhoenixWeb.PageController"} 15 phoenix_controller_call_duration_microseconds_sum{action="index",controller="SamplePhoenixWeb.PageController"} 7902.988
無事にPhoenixのメトリクス情報を確認することができましたね。
メトリクスですが、phoenix_controller_render_duration_microseconds_bucket
では呼ばれたformat、template、viewが確認でき、phoenix_controller_call_duration_microseconds_bucket
では呼ばれたactionとcontrollerが確認できます。
これだけでもメトリクスの収集としてはだいぶいい情報が集まってると思います。
Phoenixのメトリクス収集の設定ですが、今回は特にしてなかったですが設定を変更することができます。下記ドキュメントに記載されているので、もしよかったら合わせて確認してみてくださいな。
3. カスタムメトリクスに入門する
次にカスタムメトリクスを追加してみたいと思います。 今回作成してものはトップページにリクエストが来たらカウントアップするだけという単純なものになります。
早速、カウントアップ用のモジュールを用意したいと思います。
defmodule SamplePhoenix.TopHttpRequestInstrumenter do use Prometheus.Metric def setup do Counter.declare([name: :top_request_count, help: "http request count"]) end def add() do Counter.inc([name: :top_request_count]) end end
setup
メソッドでは Counter.declare
を呼びます。これで top_request_count
メトリクスのCounterを作成します。
add
メソッドでは top_request_count
メトリクスの値をインクリメントするよう、Counter.inc
を呼びます。
ですのでトップページにリクエストがきたタイミングで add
メソッドを呼ぶ処理を追加してあげれば top_request_count
メトリクスがインクリメントされるようになります。
カスタムモジュールの準備ができたのでセットアップ処理を application.ex
追加します。
def start(_type, _args) do ・・・ # 追加分 SamplePhoenix.TopHttpRequestInstrumenter.setup() SamplePhoenix.Instrumenter.setup() SamplePhoenix.PrometheusExporter.setup()
次にトップページにリクエストが来たときに呼ばれるコントローラに add
メソッドを呼ぶよう処理を追加しましょう。
def index(conn, _params) do SamplePhoenix.TopHttpRequestInstrumenter.add() render(conn, "index.html") end
これで準備が完了しました。Phoenixを起動させ、 /metrics
にアクセスしてみましょう。
# TYPE top_request_count counter # HELP top_request_count http request count top_request_count 0
まだ一度もトップページにリクエストしてないので、カウントは0の状態ですね。
試しに、5回ほど、トップページにリクエストした後にメトリクスを確認してみます。
# TYPE top_request_count counter # HELP top_request_count http request count top_request_count 5
無事にカウントアップされてるのが確認できました。 以上で、カスタムメトリクスの設定は完了となります。
補足ですが、今回カスタムメトリクスで Counter
を使ってみましたが、ほかにも Gauge
、Summary
、Histogram
Boolean
などがあります。
それぞれの用途は下記のようにあります。
- Counter: カウンタアップしていくもの(リクエスト数など)
- Gauge: 瞬間的な値(メモリ利用量など)
- Summary: 平均値(レイテンシなど)
- Histogram: データの分布などに使う(レイテンシのパーセントごとに分布させるなど)
- Boolean: True or Falseでのフラグ(ステータスなど)
4. 終わりに
今回はPhoenixでPrometheus用のExporterの設定とメトリクスの監視、カスタムメトリクスの追加まで行いました。
デフォルトのPhoenixのPrometheusのメトリクスでも十分な部分もありますが、よりアプリケーションよりのメトリクスを監視したい場合はカスタムメトリクスが必要になってくると思います。なので Counter
Gauge
Summary
Histogram
Boolean
がどのようにメトリクスを収集するのか性質を把握しておく必要があります。
ただ、カスタムメトリクスの設定自体は簡単なものだったので他のも機会を見つけて試していきたいと思います。
PrometheusでElixir Plugを監視する(Grafara設定付き)
最近、Prometheusを触り始めました。
なので何か監視したいと思い、サンプルとしてElixirのPlugの設定をし、Erlang VMの監視をしたいと思います。
今回使ったライブラリのリポジトリは下記になるので、設定とか参考にしてみてください。
また、監視として見れるものは以下に記載されていますので、確認してみてください。
目次
- 環境情報
- Plugの設定
- Prometheusの設定
- Grafanaで可視化
- 終わりに
1. 環境情報
- Elixir: 1.9.1
- Plug: 2.0
- prometheus_plugs: 1.1.1
- Prometheus: 2.15.1
- Grafana: 6.5.2
2. Plugの設定
まずはPlugの設定をしていきます。 とりあえず、ルーティングの設定をしたものになります。
また、今回はPlugについての説明は省いていくのでご了承くださいな。
defmodule SampleWeb.Router do use Plug.Router plug SampleWeb.MetricsExporter plug :match plug :dispatch get "/" do send_resp(conn, 200, "Welcome") end match _ do send_resp(conn, 404, "Oops!") end end
Plugが起動できて、HTTPリクエストが受けられるか確認するため /
にアクセスが来たら Welcome
を返すだけのものを作成しました。
ここでは plug SampleWeb.MetricsExporter
を追加しました。
これは /metrics
にアクセスが来たらメトリクスを返すような設定になります。
では、 SampleWeb.MetricsExporter
の中を見てみましょう。
defmodule SampleWeb.MetricsExporter do use Prometheus.PlugExporter end
設定としてはこれだけです。 use Prometheus.PlugExporter
を記載するだけという非常にシンプルなものになります。
次に Application
での設定をしていきます。
defmodule SampleWeb.Application do use Application def start(_type, _args) do children = [ {Plug.Cowboy, scheme: :http, plug: SampleWeb.Router, options: [port: 8080]} ] SampleWeb.MetricsExporter.setup() opts = [strategy: :one_for_one, name: SampleWeb.Supervisor] Supervisor.start_link(children, opts) end end
ここでは SampleWeb.MetricsExporter.setup()
を呼び出すだけで完了になります。これでPlugの設定が完了しましたので、次にPrometheusの設定をしていきます。
3. Prometheusの設定
Prometheusでは以下の設定をしていきます。
scrape_configs: - job_name: 'elixir_plug' metrics_path: /metrics static_configs: - targets: ['192.168.33.101:8080']
これでひとまず準備完了。では実際にPrometheusの画面で見てみましょう。
無事にメトリクスが見れるようになりましたね。
4. Grafanaで可視化
Prometheusで集めたメトリクス情報をGrafarで可視化したいと思います。 まずは、Grafaraにログインし、データソースを追加します。
Name
には Elixir Plug
とし、URL
部分に起動中のPrometheusのURLを入れます。
これで設定が完了しました。
では早速、可視化してみましょう。とりあえず、 erlang_vm_allocators
で instance_no=0
のものを見てみたいと思います。
とりあえず、可視化することに成功しました。やったぜ。
次に erlang_vm_memory_bytes_total
で kind="processes"
を見てみたいと思います。これで現在割り当てられているメモリの量が確認することができます。
Erlang VMのいろんなメトリクス情報を可視化することができました。
5. 終わりに
簡単ではありましたが、PrometheusでErlang VM監視用のElixir Plugの設定とGrafanaの可視化までやってみました。 可視化については簡単にどのように使うかまでしか見てませんので、実運用になってきたらメモリ関係はしっかりと監視したいですね。
Prometheusではいろんな言語でClientライブラリがあり、その中でElixirのメトリクス監視をやってみました。Plugの設定箇所は少なかったのですが、ちょっと苦戦してしまいました。 特にElixirですとPhoenixを使いたいところですが、今回の設定をそのままやってみてもうまくいきませんでした。。。
機会があるときにPhoenixでの設定に挑戦してみたいと思います。