AWS Storage Gateway の Amazon S3 File Gateway をプライベートサブネットに構築する

AWS

2023.9.8

Topics

先日、EC2 に構成した Storage Gateway アプライアンスで障害が発生した場合を想定し、復旧(再構築)手順を確認する機会がありました。
今まであまり Storage Gateway に触れる機会がなかったため、改めて構築手順をまとめます。

AWS Storage Gateway の種類

Storage Gateway と一口に言っても種類と構成パターンが多岐にわたりますので、まずは簡単に整理します。
2023 年 9 月現在、以下 4 種類の Storage Gateway が提供されています。

  • ファイルストレージ :S3 File Gateway、FSx File Gateway
  • ブロックストレージ :Volume Gateway
  • 仮想テープライブラリ:Tape Gateway

「S3 File Gateway」と「FSx File Gateway」は NFS もしくは SMB、「Volume Gateway」と「Tape Gateway」は iSCSI で、それぞれオンプレミスと S3 を接続します。
オンプレミスのデータを、バックアップやディザスタリカバリといった目的で S3 に保管することが主な用途となります。
「FSx File Gateway」だけは毛色が異なり、FSx for Windows File Server のキャッシュ用途として利用されます。

Storage Gateway アプライアンス

オンプレミス側のクライアント と AWS 側のストレージをつなぐ、Storage Gateway の実体を「Storage Gateway アプライアンス」と呼びます。
アプライアンスは、以下 3 通りの方法で構成することができます。

  • オンプレミス側
    • 仮想マシン(VMware ESXi、Microsoft Hyper-V、Linux KVM)
    • ハードウェアアプライアンス
  • AWS側
    • EC2 インスタンス

クライアントとのレイテンシー短縮の観点から、仮想マシンやハードウェアアプライアンスを利用してオンプレミス側に配置することが推奨されています。

VMware ESXi については vSphere HA でフェイルオーバーが行えるものの、その他のアプライアンスについては冗長構成をとることはできません。
Storage Gateway そのものがバックアップ用途となるため、シビアに可用性を担保する必要はないといった理由もあるかと考えられます。
とは言え、障害が発生したら復旧させるのが世の常です。
今回は、こちらの Storage Gateway アプライアンスに障害が発生したことを想定し、ひととおりの構築手順を確認してみました。

今回の構成

前述のとおり、Storage Gateway は種類と構成パターンが多岐にわたりますが、今回は以下の構成です。

  • オンプレミスのクライアント(Linux)から NFS 接続する Storage Gateway S3 File Gateway

  • Storage Gateway アプライアンスとして EC2 インスタンスを利用

  • Storage Gateway アプライアンス EC2 はプライベートサブネットに配置、パブリック IP アドレスは持たない

今回は、図の赤枠で囲ったコンポーネントを作成していきます。

1. Storage Gateway アプライアンス用 EC2 構築

利用中の Storage Gateway アプライアンス EC2 に障害が発生したことを想定し、再構築をしてみます。
こちらの Storage Gateway はコード管理されていませんでしたので、ひとまずマネジメントコンソールから作成です。
今回はオンプレミス拠点とのプライベート接続環境となるため、パブリック IP アドレスは付与しません。
したがって、[設定をカスタマイズ]を選択し、プライベートサブネットに EC2 を起動します。

2. VPC エンドポイント作成

プライベート接続の場合は VPC エンドポイントが必要です。
既に作成済みであれば既存の VPC エンドポイントを利用可能ですが、無い場合は以下の二種類を作成します。

  • Storage Gateway 用 VPC エンドポイント(インターフェース型)
  • S3 用 VPC エンドポイント(インターフェース型)

それぞれ、セキュリティグループで以下のとおりインバウンドトラフィックを許可します。

  • Storage Gateway 用 VPC エンドポイントのセキュリティグループ
    • Storage Gateway アプライアンスからの TCP ポート 443、1026、1027、1028、1031、2222
  • S3 用 VPC エンドポイントのセキュリティグループ
    • Storage Gateway アプライアンスからの TCP ポート 443

Amazon VPC エンドポイントを使用してゲートウェイをアクティブ化しようとすると、Storage Gateway のアクティブ化が失敗するのはなぜですか?

3. アクティベーションキー取得

今回はパブリック IP アドレスを付与しませんので、「アクティベーションキー」を取得する必要があります。

構築した EC2 インスタンスに HTTP 接続可能な環境を用意します。
構築した EC2 インスタンスのセキュリティグループで HTTP 接続が許可されていることを確認し、curl を実行してアクティベーションキーを取得します。

Getting an activation key for your gateway

上記の公式ドキュメントに従い、実際の値に置き換えて curl を実行します。

curl "http://***.***.***.***/?activationRegion=ap-northeast-1&vpcEndpoint=vpce-*********.storagegateway.ap-northeast-1.vpce.amazonaws.com&no_redirect"

4. キャッシュストレージ追加

無事アクティベーションキーが適用できましたら、Storage Gateway アプライアンス用 EC2 にキャッシュストレージを追加する必要があります。
マネジメントコンソールでは、以下のようにエラーが表示されますので、EBS で 150 GiB 分のボリュームを作成し、アタッチしましょう。

5. ファイル共有作成

デフォルトのクイック作成画面では S3 の VPC エンドポイントを指定してファイル共有を作成できないため、[設定をカスタマイズ] を選択し作成します。

ファイル共有のステータスが「利用可能」となりましたら、mount して S3 と接続できることを確認します。

$ sudo mount -t nfs -o nolock,hard 10.***.***.***:/<接続先S3バケット> /mnt

$ ll /mnt/
total 1
drwxrwxrwx. 1 nobody nobody  0 Sep  5 00:55 tmp
drwxrwxrwx. 1 nobody nobody  0 Sep  5 00:55 test
drwxrwxrwx. 1 nobody nobody  0 Sep  5 00:55 test_new
-rw-r--r--. 1 nobody nobody 13 Sep  3 12:26 test.txt

コードで管理

マネジメントコンソールで手順を振り返ってみましたが、素早く再構築するためには、やはりコードで管理するが吉です。
以下は、今回の環境を Terraform で import した場合のサンプルとなります。(Terraform v1.5.6 / provider aws v5.15.0)
あくまでサンプルとなりますので、実運用ではアクセス許可まわりなど厳密に設定する必要があります。
また、VPC、サブネット、S3 バケットについては省略しています。

ec2.tf
data "aws_ami" "sgw_ami" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["aws-storage-gateway-*"]
  }
}

resource "aws_instance" "example" {
  ami                         = data.aws_ami.sgw_ami.id
  associate_public_ip_address = false
  availability_zone           = "ap-northeast-1a"
  instance_type               = "m5.xlarge"
  subnet_id                   = "<既存のサブネット ID>"

  tags = {
    Name = "terraform-sgw"
  }

  tags_all = {
    Name = "terraform-sgw"
  }

  vpc_security_group_ids = [aws_security_group.example_sgw_ec2.id]

  ebs_block_device {
    delete_on_termination = false
    device_name           = "/dev/sdf"
    iops                  = 3000
    tags = {
      Name = "terraform-sgw-cache"
    }
    throughput  = 125
    volume_size = 150
    volume_type = "gp3"
  }

  root_block_device {
    delete_on_termination = true
    iops                  = 3000
    throughput            = 125
    volume_size           = 80
    volume_type           = "gp3"
  }
}

sg.tf
# ----------------------------------------
# Security Group - storagegateway EC2
# ----------------------------------------

resource "aws_security_group" "example_sgw_ec2" {
  name   = "sgw-ec2-security-group"
  vpc_id = "<既存のVPC ID>"
}

resource "aws_security_group_rule" "nfs_tcp" {
  type              = "ingress"
  from_port         = 2049
  to_port           = 2049
  protocol          = "tcp"
  description       = "NFS-TCP"
  cidr_blocks       = ["<既存のVPC CIDR>"]
  security_group_id = aws_security_group.example_sgw_ec2.id
}

resource "aws_security_group_rule" "nfs_udp" {
  type              = "ingress"
  from_port         = 2049
  to_port           = 2049
  protocol          = "udp"
  description       = "NFS-UDP"
  cidr_blocks       = ["<既存のVPC CIDR>"]
  security_group_id = aws_security_group.example_sgw_ec2.id
}

resource "aws_security_group_rule" "nfs_portmapper_tcp" {
  type              = "ingress"
  from_port         = 111
  to_port           = 111
  protocol          = "tcp"
  description       = "NFS-portmapper-TCP"
  cidr_blocks       = ["<既存のVPC CIDR>"]
  security_group_id = aws_security_group.example_sgw_ec2.id
}

resource "aws_security_group_rule" "nfs_portmap_udp" {
  type              = "ingress"
  from_port         = 111
  to_port           = 111
  protocol          = "udp"
  description       = "NFS-portmapper-UDP"
  cidr_blocks       = ["<既存のVPC CIDR>"]
  security_group_id = aws_security_group.example_sgw_ec2.id
}


resource "aws_security_group_rule" "nfs_v3_tcp" {
  type              = "ingress"
  from_port         = 20048
  to_port           = 20048
  protocol          = "tcp"
  description       = "NFSv3-TCP"
  cidr_blocks       = ["<既存のVPC CIDR>"]
  security_group_id = aws_security_group.example_sgw_ec2.id
}

resource "aws_security_group_rule" "nfs_v3_udp" {
  type              = "ingress"
  from_port         = 20048
  to_port           = 20048
  protocol          = "UDP"
  description       = "NFSv3-UDP"
  cidr_blocks       = ["<既存のVPC CIDR>"]
  security_group_id = aws_security_group.example_sgw_ec2.id
}

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

# ----------------------------------------
# Security Group - storagegateway VPC endpoint
# ----------------------------------------

resource "aws_security_group" "example_sgw_vpce" {
  name   = "sgw-vpce-security-group"
  vpc_id = "<既存のVPC ID>"
}

resource "aws_security_group_rule" "tcp_1026_1028" {
  type                     = "ingress"
  from_port                = 1026
  to_port                  = 1028
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.example_sgw_ec2.id
  security_group_id        = aws_security_group.example_sgw_vpce.id
}

resource "aws_security_group_rule" "tcp_1031" {
  type                     = "ingress"
  from_port                = 1031
  to_port                  = 1031
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.example_sgw_ec2.id
  security_group_id        = aws_security_group.example_sgw_vpce.id
}

resource "aws_security_group_rule" "tcp_2222" {
  type                     = "ingress"
  from_port                = 2222
  to_port                  = 2222
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.example_sgw_ec2.id
  security_group_id        = aws_security_group.example_sgw_vpce.id
}

resource "aws_security_group_rule" "tcp_443_sgwvpce" {
  type                     = "ingress"
  from_port                = 443
  to_port                  = 443
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.example_sgw_ec2.id
  security_group_id        = aws_security_group.example_sgw_vpce.id
}

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

# ----------------------------------------
# Security Group - S3 VPC endpoint
# ----------------------------------------

resource "aws_security_group" "example_s3_vpce" {
  name   = "s3-vpce-security-group"
  vpc_id = "<既存のVPC ID>"
}

resource "aws_security_group_rule" "tcp_443_s3vpce" {
  type                     = "ingress"
  from_port                = 443
  to_port                  = 443
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.example_sgw_ec2.id
  security_group_id        = aws_security_group.example_s3_vpce.id
}

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

storage-gateway.tf
resource "aws_storagegateway_gateway" "example" {
  activation_key       = "<curl で取得したアクティベーションキー>"
  gateway_name         = "terraform-sgw"
  gateway_timezone     = "GMT+9:00"
  gateway_type         = "FILE_S3"
  gateway_vpc_endpoint = lookup(aws_vpc_endpoint.sgw_example.dns_entry[0], "dns_name")

  maintenance_start_time {
    day_of_month   = null
    day_of_week    = "2"
    hour_of_day    = 0
    minute_of_hour = 10
  }
}

data "aws_storagegateway_local_disk" "sgw" {
  gateway_arn = aws_storagegateway_gateway.example.arn
  disk_node   = "/dev/sdf"
  disk_path   = "/dev/sdf"
}
resource "aws_storagegateway_cache" "sgw" {
  gateway_arn = aws_storagegateway_gateway.example.arn
  disk_id     = data.aws_storagegateway_local_disk.sgw.id
}

vpc-endpoint.tf
resource "aws_vpc_endpoint" "s3_example" {
  ip_address_type     = "ipv4"
  private_dns_enabled = true
  security_group_ids  = [aws_security_group.example_s3_vpce.id]
  service_name        = "com.amazonaws.ap-northeast-1.s3"
  subnet_ids          = ["<既存のサブネット ID>"]

  tags = {
    Name = "s3-interface-endpoint"
  }

  vpc_endpoint_type = "Interface"
  vpc_id            = "<既存のVPC ID>"

  dns_options {
    dns_record_ip_type = "ipv4"
  }
}

resource "aws_vpc_endpoint" "sgw_example" {
  ip_address_type     = "ipv4"
  private_dns_enabled = true
  security_group_ids  = [aws_security_group.example_sgw_vpce.id]
  service_name        = "com.amazonaws.ap-northeast-1.storagegateway"
  subnet_ids          = ["<既存のサブネット ID>"]

  tags = {
    Name = "sgw-interface-endpoint"
  }

  vpc_endpoint_type = "Interface"
  vpc_id            = "<既存のVPC ID>"

  dns_options {
    dns_record_ip_type = "ipv4"
  }
}

file-share.tf
resource "aws_storagegateway_nfs_file_share" "example" {
  client_list           = ["0.0.0.0/0"]
  gateway_arn           = aws_storagegateway_gateway.example.arn
  location_arn          = "<既存のS3バケット ARN>"
  bucket_region         = "ap-northeast-1"
  role_arn              = aws_iam_role.example.arn
  vpc_endpoint_dns_name = substr("${lookup(aws_vpc_endpoint.s3_example.dns_entry[0], "dns_name")}", 2, -1)
}

resource "aws_iam_role" "example" {
  name = "file-share-role"

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

resource "aws_iam_role_policy_attachment" "s3" {
  role       = aws_iam_role.example.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
}

プライベートサブネットに Storage Gateway を構築する場合は EC2 インスタンス作成後にアクティベーションキーを取得するため、段階を踏んで terraform apply する必要があります。
一手間必要ではありますが、効率的で間違いのない運用のためにも、可能な限りコードで管理していきましょう!

GENSAI

2014年入社。 関東近郊の山と銭湯と飲み屋を巡り歩いてます。

Recommends

こちらもおすすめ

Special Topics

注目記事はこちら