EC2でRocky Linux9を構築する際にやったこと。

EC2でRocky Linux9を構築する際にやったこと。

2025.08.02

はじめに

皆様こんにちは、あかいけです。

あなたは Rocky Linux9 を知っていますか?
私は最近触れ合う機会があり、そこで知りました。

構築を進める中でいつもの Amazon Linux2023 と若干初期設定の方法が異なる部分が若干あったので、
今回は EC2 で Rocky Linux9 を構築する際にやったことをまとめます。

Rocky Linux ってなんだろう。

Rocky Linux は、Red Hat Enterprise Linux (RHEL) と互換性を保ちながら無償で利用できる Linux ディストリビューションです。

Red Hat 社が CentOS のサポート終了を突然発表したことを受け、CentOS の創設者である Gregory Kurtzer 氏が中心となって立ち上げたプロジェクトです。
名前は、CentOS の共同創設者でもあった Rocky McGaugh 氏に由来しています。

他のディストリビューションとの関係性

簡単に整理すると以下のような関係性になります。

  • Red Hat Enterprise Linux (RHEL): 商用の有償ディストリビューション(安定性とサポートが充実)
  • CentOS: RHEL の無償版として長年愛用されてきたが、2024 年 6 月 30 日にサポート終了
  • Rocky Linux / AlmaLinux: CentOS の代替として登場した無償ディストリビューション

Rocky Linux と AlmaLinux はどちらも CentOS の代替として優秀ですが、Rocky Linux は CentOS の創設者が立ち上げたという安心感もあり、個人的には Rocky Linux を選ぶかなと思います。

https://www.redhat.com/ja/topics/linux/centos-linux-eol

サポート期限について

Rocky Linux や AlmaLinux は RHEL とライフサイクルを合わせているためサポート期限も大体同じで、
アクティブサポートが約 5 年、セキュリティーサポートも含めたトータルのサポート期間が約 10 年となっています。

ただし Rocky Linux は RHEL の Extended Life Cycle は Rocky Linux はサポートしていないため、RHEL と比べるとサポート期限は短くなります。

https://access.redhat.com/support/policy/updates/errata#Extended_Life_Cycle_Phase

具体的には Rocky Linux9 であれば以下のサポート期限です。

  • アクティブサポート:2027 年 5 月 31 日まで
  • セキュリティーサポート: 2032 年 5 月 31 日まで

https://wiki.rockylinux.org/rocky/version/

Amazon Linux2023 は以下のサポート期間のため、
サポート期限を重要視する場合は Rocky Linux や RHEL や AlmaLinux を選ぶのもありかなと思います。

  • スタンダードサポート:2027 年 6 月 30 日まで
  • セキュリティーサポート:2029 年 6 月 30 日まで

また本記事と直接関係ないですが、以下サイトにいろんな製品の EOL がまとまっていてとても有益でした。
ありがとう、ありがとう…。

https://endoflife.date/

事前作業

サブスクリプション

Rocky Linux の公式から無償の AMI が提供されており、
まずはサブスクリプション契約しましょう。

https://aws.amazon.com/marketplace/pp/prodview-ygp66mwgbl2ii?applicationId=AWSMPContessa&ref_=beagle&sr=0-1

EC2 作成時に契約してもいいですが、今回は Terraform で作成したいので先に AMI カタログで契約しておきます。

https://ap-northeast-1.console.aws.amazon.com/ec2/home?region=ap-northeast-1#AMICatalog:

検索条件はAMI名で「Rocky Linux」、パブリッシャーも「Rocky Linux」を選択して出てくるものです。

スクリーンショット 2025-07-31 17.07.43

AMIを選択したら「今すぐ購読」をクリックするとサブスクリプション契約ができます。

スクリーンショット 2025-07-31 17.08.14

実際にアクティブになっているサブスクリプションは以下で確認できます。

https://us-east-1.console.aws.amazon.com/marketplace/subscriptions?region=us-east-1

スクリーンショット 2025-07-31 17.09.40

作成後の作業

SSM Agent

とりあえず SSM Agent を入れましょう。
ドンピシャなドキュメントがあるのでこちらを参考にします。

https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/agent-install-rocky.html

ドキュメント通りですが、以下のコマンドを実行すればインストール完了です。

sudo dnf install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm
sudo systemctl enable amazon-ssm-agent

CloudWatch Agent

次に CloudWatch Agent です。
ただ Rocky Linux 用の rpm は用意されていないので、RHEL 用の rpm を利用します。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/download-cloudwatch-agent-commandline.html

インストールコマンドは以下の通り。

sudo dnf install -y https://amazoncloudwatch-agent.s3.amazonaws.com/redhat/amd64/latest/amazon-cloudwatch-agent.rpm
sudo systemctl enable amazon-cloudwatch-agent

設定コマンドは以下の通り。
事前にパラメータストア (AmazonCloudWatch-Parameter-EC2) に設定ファイルを格納しておき、
それを読み取りに行っています。

sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
  -a fetch-config \
  -m ec2 \
  -c ssm:AmazonCloudWatch-Parameter-EC2 \
  -s

以下はパラメータストアの例で、設定内容は以下の通りです。

  • ログ
    • /var/log/messages (OSログ)
  • カスタムメトリクス
    • mem_used_percent (メモリ使用率)
    • disk_used_percent (ディスク使用率)
AmazonCloudWatch-Parameter-EC2
{
  "logs": {
    "logs_collected": {
      "files": {
        "collect_list": [
          {
            "file_path": "/var/log/messages",
            "log_group_name": "/aws/ec2/rocky-test/var-log",
            "log_stream_name": "{instance_id}",
            "timestamp_format": "%Y-%m-%d %H:%M:%S.%f"
          }
        ]
      }
    }
  },
  "metrics": {
    "aggregation_dimensions": [
      ["InstanceId"]
    ],
    "append_dimensions": {
      "AutoScalingGroupName": "${aws:AutoScalingGroupName}",
      "ImageId": "${aws:ImageId}",
      "InstanceId": "${aws:InstanceId}",
      "InstanceType": "${aws:InstanceType}"
    },
    "metrics_collected": {
      "disk": {
        "measurement": [
          "used_percent"
        ],
        "metrics_collection_interval": 60,
        "resources": [
          "/"
        ]
      },
      "mem": {
        "measurement": [
          "used_percent"
        ],
        "metrics_collection_interval": 60
      }
    }
  }
}

なお設定ファイルの詳細な情報は以下をご参照ください。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-Configuration-File-Details.html

またデフォルトで利用可能なカスタムメトリクスについては以下をご参照ください。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/metrics-collected-by-CloudWatch-agent.html

その他もろもろ

このあたりは他の Linux ディストリビューションでも同じなので、細かい説明は割愛します。

  • パッケージ アップグレード
sudo dnf -y upgrade
  • タイムゾーン
sudo timedatectl set-timezone Asia/Tokyo
  • ホスト名
sudo hostnamectl set-hostname rocky-test-ec2
  • SELinux
    デフォルトだと enforcing のため必要に応じて変更する。
sudo sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config

変更後に再起動して反映する。

sudo reboot

全部まとめたユーザーデータ

今までの内容をまとめたユーザーデータは以下の通りです。

user-data.sh
#!/bin/bash
set -eux

# Update Package
sudo dnf -y upgrade

# Change Timezone To JST
timedatectl set-timezone Asia/Tokyo

# Set Hostname
sudo hostnamectl set-hostname rocky-test-ec2

# Install SSM Agent
sudo dnf install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm
sudo systemctl enable amazon-ssm-agent

# Install CloudWatch Agent
sudo dnf install -y https://amazoncloudwatch-agent.s3.amazonaws.com/redhat/amd64/latest/amazon-cloudwatch-agent.rpm
sudo systemctl enable amazon-cloudwatch-agent

# Configure CloudWatch Agent
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
  -a fetch-config \
  -m ec2 \
  -c ssm:AmazonCloudWatch-Parameter-EC2 \
  -s

# Setting SELinux
sudo sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config

sudo reboot

構築してみる

Terraform で作る

長いので閉じておきます。

Terraform
main.tf
terraform {
  required_version = ">= 1.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 6.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
  default_tags {
    tags = {
      app = var.app_name
    }
  }
}

# Variables
variable "app_name" {
  default = "rocky-test"
}

variable "aws_region" {
  description = "AWS region"
  default     = "ap-northeast-1"
  type        = string
}

variable "vpc_cidr" {
  description = "CIDR block for VPC"
  default     = "172.16.0.0/16"
  type        = string
}

variable "public_subnets" {
  description = "Public subnet configurations"
  type = map(object({
    cidr_block        = string
    availability_zone = string
  }))
  default = {
    public-subnet-1a = {
      cidr_block        = "172.16.0.0/24"
      availability_zone = "ap-northeast-1a"
    }
  }
}

# VPC
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "${var.app_name}-vpc"
  }
}

# Subnet
resource "aws_subnet" "public" {
  for_each                = var.public_subnets
  vpc_id                  = aws_vpc.main.id
  cidr_block              = each.value.cidr_block
  map_public_ip_on_launch = true
  availability_zone       = each.value.availability_zone
  tags = {
    Name = "${var.app_name}-${each.key}"
  }
}

# IGW
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
  tags = {
    Name = "${var.app_name}-igw"
  }
}

# Route Table
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }
  tags = {
    Name = "${var.app_name}-public-rtb"
  }
}

resource "aws_route_table_association" "public" {
  for_each       = var.public_subnets
  subnet_id      = aws_subnet.public[each.key].id
  route_table_id = aws_route_table.public.id
}

# NACLs
resource "aws_network_acl" "public" {
  vpc_id     = aws_vpc.main.id
  subnet_ids = [for s in aws_subnet.public : s.id]

  ingress {
    protocol   = "-1"
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }

  egress {
    protocol   = "-1"
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }

  tags = {
    Name = "${var.app_name}-public-nacl"
  }
}

# SG
resource "aws_security_group" "main" {
  name        = "${var.app_name}-ec2-sg"
  description = "Security group for EC2"
  vpc_id      = aws_vpc.main.id

  tags = {
    Name = "${var.app_name}-ec2-sg"
  }
}

resource "aws_security_group_rule" "main" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.main.id
}

# Rocky Linux 9 AMI
data "aws_ami" "rocky9" {
  most_recent = true
  owners      = ["679593333241"] # Rocky Linux official

  filter {
    name   = "name"
    values = ["Rocky-9-EC2-Base-*"]
  }

  filter {
    name   = "architecture"
    values = ["x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

resource "aws_instance" "main" {
  ami                    = data.aws_ami.rocky9.id
  instance_type          = "t3.small"
  subnet_id              = aws_subnet.public["public-subnet-1a"].id
  vpc_security_group_ids = [aws_security_group.main.id]
  iam_instance_profile   = aws_iam_instance_profile.main.name

  root_block_device {
    volume_type           = "gp3"
    volume_size           = 20
    iops                  = 3000
    throughput            = 125
    encrypted             = true
    delete_on_termination = true
    tags = {
      Name = "${var.app_name}-ebs"
    }
  }

  metadata_options {
    http_endpoint = "enabled"
    http_tokens   = "required"
  }

  user_data = base64encode(templatefile("${path.module}/conf/user_data.sh", {}))

  tags = {
    Name             = "${var.app_name}-ec2"
    cm-opswitchStart = "true"
    cm-opswitchStop  = "true"
  }

  depends_on = [aws_ssm_parameter.cloudwatch_config]
}

output "ssm_start_session" {
  value = "aws ssm start-session --target ${aws_instance.main.id}"
}

# CloudWatch Logs Group
resource "aws_cloudwatch_log_group" "main" {
  name              = "/aws/ec2/${var.app_name}-ec2/var-log"
  retention_in_days = 180

  tags = {
    Name = "/aws/ec2/${var.app_name}-ec2/var-log"
  }
}

# CloudWatch Agent configuration in SSM Parameter Store
resource "aws_ssm_parameter" "cloudwatch_config" {
  name  = "AmazonCloudWatch-Parameter-EC2"
  type  = "String"
  value = <<-EOT
  {
    "logs": {
      "logs_collected": {
        "files": {
          "collect_list": [
            {
              "file_path": "/var/log/messages",
              "log_group_name": "${aws_cloudwatch_log_group.main.name}",
              "log_stream_name": "{instance_id}",
              "timestamp_format": "%Y-%m-%d %H:%M:%S.%f"
            }
          ]
        }
      }
    },
    "metrics": {
      "aggregation_dimensions": [
        ["InstanceId"]
      ],
      "append_dimensions": {
        "AutoScalingGroupName": "$${aws:AutoScalingGroupName}",
        "ImageId": "$${aws:ImageId}",
        "InstanceId": "$${aws:InstanceId}",
        "InstanceType": "$${aws:InstanceType}"
      },
      "metrics_collected": {
        "disk": {
          "measurement": [
            "used_percent"
          ],
          "metrics_collection_interval": 60,
          "resources": [
            "/"
          ]
        },
        "mem": {
          "measurement": [
            "used_percent"
          ],
          "metrics_collection_interval": 60
        }
      }
    }
  }
  EOT

  tags = {
    Name = "AmazonCloudWatch-Parameter-EC2"
  }
}

# ec2用IAMロール
resource "aws_iam_role" "main" {
  name = "${var.app_name}-ec2-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      }
    ]
  })

  tags = {
    Name = "${var.app_name}-ec2-role"
  }
}

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

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

resource "aws_iam_instance_profile" "main" {
  name = "${var.app_name}-instance-profile"
  role = aws_iam_role.main.name
}
conf/user_data.sh
#!/bin/bash
set -eux

# Update Package
sudo dnf -y upgrade

# Change Timezone To JST
timedatectl set-timezone Asia/Tokyo

# Set Hostname
sudo hostnamectl set-hostname rocky-test-ec2

# Install SSM Agent
sudo dnf install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm
sudo systemctl enable amazon-ssm-agent

# Install CloudWatch Agent
sudo dnf install -y https://amazoncloudwatch-agent.s3.amazonaws.com/redhat/amd64/latest/amazon-cloudwatch-agent.rpm
sudo systemctl enable amazon-cloudwatch-agent

# Configure CloudWatch Agent
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
  -a fetch-config \
  -m ec2 \
  -c ssm:AmazonCloudWatch-Parameter-EC2 \
  -s

# Setting SELinux
sudo sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config

sudo reboot

リソースの設定自体はよくある内容なので、重要なのは AMI の指定の箇所かなと思います。

# Rocky Linux 9 AMI
data "aws_ami" "rocky9" {
  most_recent = true
  owners      = ["679593333241"] # Rocky Linux official

  filter {
    name   = "name"
    values = ["Rocky-9-EC2-Base-*"]
  }

  filter {
    name   = "architecture"
    values = ["x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

フィルターの条件は、マネジメントコンソールの EC2 作成画面でAMIを選択して確認しました。

スクリーンショット 2025-08-02 11.53.31

小ネタ

初期ユーザー名について

デフォルトで作成されるユーザーは「rocky」です。
Amazon Linux であれば ec2-user、Ubuntu であれば ubuntu、みたいな感じですね。

rsyslog について

Amazon Linux2023 だとデフォルトで rsyslog が入っていないのですが、
Rocky Linux9 だとデフォルトで rsyslog が入っています。

結局 Amazon Linux2023 でも OS ログは/var/log/messages を使うことが多い気がするので、
個人的には嬉しいポイントです。

さいごに

以上、EC2 で Rocky Linux9 を構築する際にやったことでした。
普段は Amazon Linux か RHEL、または Windows、たまに Ubuntu しか触らないので、
たまには別の Linux ディストリビューションを触るのもリフレッシュになっていいなと感じました。

正直最初は「また新しいディストリビューション覚えるのかぁ…」とちょっと億劫だったのですが、実際に触ってみると Amazon Linux2023 との違いはそれほど大きくなく、むしろ rsyslog がデフォルトで入っているなど「お、これは便利だな」と思える部分もありました。

CentOS がなくなって困っている方、RHEL のライセンス費用が気になる方、または単純に新しいものを試してみたい方は、ぜひ Rocky Linux を触ってみてください。

それでは、また次の記事でお会いしましょう。

この記事をシェアする

facebookのロゴhatenaのロゴtwitterのロゴ

© Classmethod, Inc. All rights reserved.