SSM State Managerを使ってTerraformで作成したEC2インスタンスの初期化をしてみた

SSM State Managerを使うことで、Terraformの初期構築時にEC2インスタンスでAnsibleを実行します。
2023.05.18

AWS事業本部のイシザワです。

コンソールやIaCでEC2インスタンス内のOSの設定やパッケージインストール、アプリケーションデプロイ等々のシステム層以上の設定をする方法はいくつかあります。

User dataを使う方法やCloudFormationでcfn-initを使う方法もありますが、今回はSSM State Managerを使ってEC2インスタンスにNGINXをインストールしたいと思います。

cfn-initでなくSSM State Managerを使う利点は以下のAWSブログが詳しいです。

CloudFormation で cfn-init に代えて State Manager を利用する方法とその利点

以下はブログからの抜粋です。

State Managerを使用すると、cfn-initと同様に多くのタスクを実行できますが、さらに追加のメリットがあります。

  1. ログをAmazon Simple Storage Service (S3)に集約
  2. CloudFormationテンプレートの簡素化
  3. 設定コンプライアンスの可視性の向上
  4. AWS CloudTrailと統合され改善された監査
  5. 初期プロビジョニング後のスケジュールに基づく設定
  6. タグを使用して起動された新しいインスタンスの設定
  7. 同時実行値とエラーしきい値の設定

ソースコード

Terraformのソースコードは以下のGitHubレポジトリにあります。

ソースコード

SSM State ManagerでEC2インスタンスへの関連付けを作成することで、初期構築時にインスタンスに対してAnsibleを実行します。 Ansibleのプレイブックはリポジトリ内にあります。

今回の構成ではState ManagerはS3バケットからAnsibleプレイブックを取得するよう設定しています。 そのため、AnsibleのプレイブックをZIPで圧縮してS3バケットにアップロードする必要があるのですが、Terraformを使えばこの工程も含めて一撃でシステムを構築することができます。

ソースコードの解説

ソースコードの一部をピックアップして解説します。

Ansibleプレイブックのアップロード

data.archive_file.ansibleでローカルのAnsibleプレイブックをZIPに圧縮し、ZIPファイルをaws_s3_object.ansibleでS3バケットにアップロードします。

s3.tf

data "archive_file" "ansible" {
  type        = "zip"
  source_dir  = "./ansible"
  output_path = local.playbook_archive_file_path
}

resource "aws_s3_object" "ansible" {
  bucket = module.ansible_bucket.name
  key    = local.playbook_file_name
  source = local.playbook_archive_file_path
  etag   = data.archive_file.ansible.output_md5

  depends_on = [
    module.ansible_bucket
  ]
}

IAMロール

EC2インスタンスにはS3バケットからAnsibleプレイブックを取得する権限と、ログ保管用S3バケットにSSMのログファイルを格納する権限が必要です。 SSMの機能を使用するため、IAMロールにAWS管理ポリシーのAmazonSSMManagedInstanceCoreをアタッチする必要もあります。

iam.tf

data "aws_iam_policy_document" "read_bucket" {
  statement {
    effect = "Allow"
    actions = [
      "s3:GetObject",
    ]
    resources = [
      "${module.ansible_bucket.arn}/*",
    ]
  }

  statement {
    effect = "Allow"
    actions = [
      "s3:GetObject",
      "s3:PutObject",
      "s3:PutObjectAcl",
    ]
    resources = [
      "${module.ssm_log_bucket.arn}/*",
    ]
  }
}

resource "aws_iam_policy" "read_bucket" {
  name   = "read_bucket_policy"
  policy = data.aws_iam_policy_document.read_bucket.json
}

resource "aws_iam_role" "web_server" {
  name               = "web_server_role"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
  managed_policy_arns = [
    aws_iam_policy.read_bucket.arn,
    "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore",
  ]
}

SSM State Manager

State Managerの関連付けを作成します。ソースファイルにS3にアップロードしたZIPファイルを指定します。

ssm.tf

resource "aws_ssm_association" "ansible" {
  name = "AWS-ApplyAnsiblePlaybooks"

  parameters = {
    SourceType = "S3",
    SourceInfo = jsonencode({
      path = "https://s3.amazonaws.com/${module.ansible_bucket.name}/${local.playbook_file_name}"
    }),
    PlaybookFile        = "site.yml",
    InstallDependencies = "True",
  }

  targets {
    key    = "InstanceIds"
    values = [aws_instance.web_server.id]
  }

  output_location {
    s3_bucket_name = module.ssm_log_bucket.name
  }

  depends_on = [
    aws_s3_object.ansible
  ]
}

動作確認

以下はterraform applyコマンドの実行結果です。IPアドレスはマスクしています。

$ terraform apply

(中略)

Apply complete! Resources: 18 added, 0 changed, 0 destroyed.

Outputs:

instance_ip = "aaa.bbb.ccc.ddd"

State Managerの関連付けの実行に数分かかります。しばらく経ってからcurlコマンドで↑で出力されたIPに対してHTTPリクエストをするとレスポンスが返ってきます。

$ curl http://aaa.bbb.ccc.ddd
<html><h1>Hello World!</h1></html>

終わりに

SSM State Managerを使うことで、Terraformでの初期構築時にEC2インスタンスの初期化をしてみました。

この記事が誰かのお役に立てれば幸いです。