TerraformでAWS Storage Gateway (S3 File Gateway)を作ってみた

2021.09.18

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

しばたです。

先日検証用にStorage Gateway (S3 File Gateway)を作る必要があり、どう構築しようか悩んでいたところTerraformがStorage Gatewayに対応していたので試してみることにしました。

構築環境について

今回作った環境は既存VPCのPrivate SubnetにEC2を構築、Storage GatewayはS3 File GatewayでSMBアクセス可能とするものとなります。
検証用なので認証はゲストユーザーとしています。

図としてはこんな感じです。

(Storage Gateway APIエンドポイントへのアクセスのためPrivate Subnetからインターネット通信可能にしています)

構築用tfファイルはGistにアップロードしています。

一応本記事にも転記しておきます。展開してご覧ください。

main.tf

data "aws_caller_identity" "current" {}

// System name settings
variable "sysname" {
  type    = string
  default = "mysgw"
}
variable "envname" {
  type    = string
  default = "dev"
}

// VPC settings
data "aws_vpc" "vpc" {
  // Set your VPC id
  id = "vpc-1234567890"
}
data "aws_subnet" "sgw_subnet" {
  // Set your Storage Gateway subnet id
  id = "subnet-1234567890"
}

// Other settings
locals {
  // Set your EC2 key pair name
  ec2_keyname = "my-keypair"
}

provider.tf

provider "aws" {
  region = "ap-northeast-1"
}

terraform {
  backend "local" {
  }
}

storage-gateway-01.tf

//
// Storage Gatewayの構築に必要な前提リソース
//

//
// S3
//
resource "aws_s3_bucket" "sgw" {
  bucket = "${var.sysname}-${var.envname}-storage-gateway-${data.aws_caller_identity.current.account_id}"
  acl    = "private"
}
resource "aws_s3_bucket_public_access_block" "sgw" {
  bucket                  = aws_s3_bucket.sgw.bucket
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

//
// Security Group
//
resource "aws_security_group" "sgw" {
  vpc_id      = data.aws_vpc.vpc.id
  name        = "${var.sysname}-${var.envname}-storage-gateway-sg"
  description = "Security group for Storage Gateway"
  // HTTP for activation
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    description = "HTTP for activation"
    cidr_blocks = [data.aws_vpc.vpc.cidr_block]
  }
  // SMB
  ingress {
    from_port   = 445
    to_port     = 445
    protocol    = "tcp"
    description = "SMB"
    cidr_blocks = [data.aws_vpc.vpc.cidr_block]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = {
    Name = "${var.sysname}-${var.envname}-storage-gateway-sg"
  }
}

//
// EC2
//
data "aws_ssm_parameter" "sgw_ami" {
  name = "/aws/service/storagegateway/ami/FILE_S3/latest"
}
resource "aws_instance" "sgw" {
  instance_type               = "m5.xlarge" // AWS推奨スペック
  ami                         = nonsensitive(data.aws_ssm_parameter.sgw_ami.value)
  subnet_id                   = data.aws_subnet.sgw_subnet.id
  key_name                    = local.ec2_keyname
  vpc_security_group_ids      = [aws_security_group.sgw.id]
  iam_instance_profile        = ""    // Instance profileなし
  disable_api_termination     = false // 検証用なので削除可
  associate_public_ip_address = false // Private subnetに配備
  root_block_device {
    volume_type           = "gp3"
    iops                  = 3000 // default value 
    throughput            = 125  // default value
    volume_size           = 80   // AMIのデフォルト値
    delete_on_termination = true
    encrypted             = false
    tags = {
      Name = "${var.sysname}-${var.envname}-storage-gateawy-root"
    }
  }
  ebs_block_device {
    // キャッシュ用EBS
    device_name           = "/dev/sdf"
    volume_type           = "gp3"
    iops                  = 3000 // default value 
    throughput            = 125  // default value
    volume_size           = 150  // 最低容量
    delete_on_termination = true
    encrypted             = false
    tags = {
      Name = "${var.sysname}-${var.envname}-storage-gateawy-cache"
    }
  }
  tags = {
    Name = "${var.sysname}-${var.envname}-storage-gateway"
  }
  lifecycle {
    ignore_changes = [associate_public_ip_address]
  }
}

storage-gateway-02.tf

//
// Storage Gateway関連のリソース
//

//
// IAM role for Storage Gateway file share
//
data "aws_iam_policy_document" "sgw_fileshare" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["storagegateway.amazonaws.com"]
    }
  }
}
resource "aws_iam_policy" "sgw_fileshare" {
  name = "${var.sysname}-${var.envname}-storage-gateawy-fileshare"
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "s3:GetAccelerateConfiguration",
          "s3:GetBucketLocation",
          "s3:GetBucketVersioning",
          "s3:ListBucket",
          "s3:ListBucketVersions",
          "s3:ListBucketMultipartUploads"
        ]
        Effect   = "Allow"
        Resource = "${aws_s3_bucket.sgw.arn}"
      },
      {
        Action = [
          "s3:AbortMultipartUpload",
          "s3:DeleteObject",
          "s3:DeleteObjectVersion",
          "s3:GetObject",
          "s3:GetObjectAcl",
          "s3:GetObjectVersion",
          "s3:ListMultipartUploadParts",
          "s3:PutObject",
          "s3:PutObjectAcl"
        ]
        Effect   = "Allow"
        Resource = "${aws_s3_bucket.sgw.arn}/*"
      }
    ]
  })
}
resource "aws_iam_role" "sgw_fileshare" {
  name               = "${var.sysname}-${var.envname}-storage-gateawy-fileshare"
  assume_role_policy = data.aws_iam_policy_document.sgw_fileshare.json
  managed_policy_arns = [
    aws_iam_policy.sgw_fileshare.arn
  ]
}

//
// Storage Gateway 
// ※ リソース作成後も以下のパラメーターは手動で設定する必要有り
//   - CloudWatch Logs設定
//   - メンテナンス時間
resource "aws_storagegateway_gateway" "sgw" {
  // gateway_ip_address = aws_instance.sgw.private_ip
  // 今回はTerraformから直接EC2へアクセスできないため事前にAtivation Keyを取得している
  activation_key     = "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"
  gateway_name       = "${var.sysname}-${var.envname}-gateway"
  gateway_timezone   = "GMT+9:00" // JST
  gateway_type       = "FILE_S3"  // S3 File Storage
  smb_guest_password = "P@ssw0rd" // ゲストユーザーパスワード
  lifecycle {
    ignore_changes = [smb_guest_password]
  }
}
// Local Cache 設定
data "aws_storagegateway_local_disk" "sgw" {
  gateway_arn = aws_storagegateway_gateway.sgw.arn
  disk_node   = "/dev/sdf"
}
resource "aws_storagegateway_cache" "sgw" {
  gateway_arn = aws_storagegateway_gateway.sgw.arn
  disk_id     = data.aws_storagegateway_local_disk.sgw.id
}

//
// SMB File share
//
resource "aws_storagegateway_smb_file_share" "sgw" {
  authentication  = "GuestAccess"
  gateway_arn     = aws_storagegateway_gateway.sgw.arn
  location_arn    = aws_s3_bucket.sgw.arn
  role_arn        = aws_iam_role.sgw_fileshare.arn
  file_share_name = "share" // 今回はFSx for Windowsと同様に共有名を share としている
  object_acl      = "bucket-owner-full-control"
}

Terraformのバージョンは

  • Terraform 1.0.5
  • Terraform AWS provider 3.58.0

で動作確認しています。

Storage Gatewayの基本と構築の限界

EC2でStorage Gatewayを構築する場合専用のAMIからEC2を構築しアクティベーションしてやる必要があります。
これが曲者で構築したEC2にHTTPアクセスしないとアクティベーションできません。

EC2がパブリックに公開されTerraformの実行環境から直接アクセス可能であれば一気通貫で環境構築ができるのですが、そうでない場合

  1. Storage Gateway用EC2を構築
  2. 構築したEC2にHTTPアクセスし、専用の「アクティベーションコード」を取得
  3. 「アクティベーションコード」を使いStorage Gatewayを構築

と段階を分けて作業してやる必要があります。
このため今回のtfファイルもstorage-gateway-01.tfstorage-gateway-02.tfと2つに分割しています。

構築手順

ここから実際に環境構築手順を解説します。

1. 前準備 (EC2, S3の構築)

前節で述べた通りPrivate SubnetにStorage Gatewayを構築する場合アクティベーションキーを取得するため二段階に分けて作業を行います。
まずはstorage-gateway-02.tfをリネームするなどして最初にstorage-gateway-01.tfだけを実行してください。

これにより

  • S3 (<system name>-<environment name>-storage-gateway-<account id>)
  • EC2 (<system name>-<environment name>-storage-gateway)
  • セキュリティグループ (<system name>-<environment name>-storage-gateway)

が作成されます。

S3の設計に関して特別なことはしていません。
EC2にはStorage Gateway用のAMIが用意されているため、SSM Parameterから最新のAMI IDを取得しています。

// SSM Parameterから最新のStorage Gateway AMIを取得
data "aws_ssm_parameter" "sgw_ami" {
  name = "/aws/service/storagegateway/ami/FILE_S3/latest"
}

またインスタンスタイプについて、ドキュメントではm4.xlarge以上を推奨しているため現行世代のm5.xlargeとしています。
加えてルートボリューム(要80GiB)に加えて必ずキャッシュ用ボリュームを用意する必要があります。 今回はgp3で最低容量の150GiBとしています。
この辺のスペックに関しては必要に応じて変更すると良いでしょう。

セキュリティグループはシンプルにVPC CIDRの範囲でHTTPとSMBを公開しています。

これからStorage Gatewayを構築するにはアクティベーションキーを取得してやる必要があります。
構築したEC2にHTTPアクセス可能な環境(今回は既存Bastion)からドキュメントの手順に従ってください。

今回はWindows ServerのBastionを使っているのでPowerShellを使い以下の様にして取得しています。

function Get-ActivationKey {
  [CmdletBinding()]
  Param(
    [parameter(Mandatory=$true)][string]$IpAddress, 
    [parameter(Mandatory=$true)][string]$ActivationRegion
  )
  PROCESS {
    $request = Invoke-WebRequest -UseBasicParsing -Uri "http://$IpAddress/?activationRegion=$ActivationRegion" -MaximumRedirection 0 -ErrorAction SilentlyContinue
    if ($request) {
      $activationKeyParam = $request.Headers.Location | Select-String -Pattern "activationKey=([A-Z0-9-]+)"
      $activationKeyParam.Matches.Value.Split("=")[1]
    }
  }
}
Get-ActivationKey -IpAddress 10.0.21.125 -ActivationRegion ap-northeast-1

25桁のキー(今回はQNI59-SJ9DE-XXXXX-XXXXX-XXXXX)を取得をできればOKです。

2. Storage Gatewayの構築

アクティベーションキーが取得できたのでstorage-gateway-02.tfに転記して実行します。

resource "aws_storagegateway_gateway" "sgw" {
  // 今回はTerraformから直接EC2へアクセスできないため事前にAtivation Keyを取得している
  activation_key     = "QNI59-SJ9DE-XXXXX-XXXXX-XXXXX"

  // snip...
}

これで

  • ファイル共有用 IAM Role (<system name>-<environment name>-storage-gateawy-fileshare)
  • Storage Gateway (<system name>-<environment name>-gateway)
  • Storege Gatewayファイル共有

が作成されます。

ここからTerraformの各種リソースについて簡単に解説します。

a. ファイル共有用 IAM Role

Storage Gatewayファイル共有ではS3アクセスのためのIAM Roleが必要となります。
以下のドキュメントに従いS3バケットへのアクセス権を持つIAM Roleを用意します。

このIAM Roleは後述のaws_storagegateway_smb_file_shareリソースで使用します。

b. aws_storagegateway_gateway リソース

Storage Gatewayはaws_storagegateway_smb_file_shareリソースで定義します。
基本的なパラメーターはドキュメントをみてください。

アクティベーションのためにgateway_ip_addressactivation_keyパラメーターのどちらかを指定する必要があります。
GatewayをPublic Subnetに配備しTerraformの実行環境からHTTPアクセス可能であればgateway_ip_addressを指定できます。
今回はPrivate Subnetに配備してるため事前に取得したactivation_keyを使う形としています。

あとはSMBに関わる設定もGateway本体で行います。
今回はゲストアクセスにしているのでsmb_guest_passwordを指定してやります。

c. aws_storagegateway_cache リソース

Storage Gatewayのキャッシュ設定はaws_storagegateway_cacheリソースで行います。

設定自体はキャッシュで使うEBSボリュームを指定してやるだけなのですが、EBSボリュームをディスクID(disk_id)で指定してやる必要があります。
EC2作成時のデバイス名(/dev/sdfなど)からIDを取得するためにaws_storagegateway_local_diskDataリソースを使ってやる必要があるのが軽いハマりポイントです。

// ディスクIDの取得のために aws_storagegateway_local_disk を使う
// 「disk_node」を以下の様に記載する
data "aws_storagegateway_local_disk" "sgw" {
  gateway_arn = aws_storagegateway_gateway.sgw.arn
  disk_node   = "/dev/sdf"
}

resource "aws_storagegateway_cache" "sgw" {
  gateway_arn = aws_storagegateway_gateway.sgw.arn
  // aws_storagegateway_local_diskからディスクIDを設定
  disk_id     = data.aws_storagegateway_local_disk.sgw.id
}

d. aws_storagegateway_smb_file_share リソース

SMBによるファイル共有はaws_storagegateway_smb_file_shareリソースで定義します。
このリソースで使用するS3(location_arn)や先述のIAM Role(role_arn)等を定義します。

細かいパラメーターに関してはドキュメントを参照してください。

3. 動作確認

今回の例では\\10.0.21.125\shareが共有フォルダとなります。
このフォルダに対しゲストユーザーsmbguestでアクセスしてやります。

ちゃんとS3に同期されています。

最後に

以上となります。

日本語情報があまりなかった様なので使用したtfファイルを雑に公開してみました。
Storage Gatewayが必要になるケース自体がそこまで多くないかもしれませんがこの記事が誰かの役に立てば幸いです。