あるシステムで閉域 VPC の中で Elastic Container Service (ECS) を Fargate で使う必要が出てきたので、早速検証してみました。
閉域 VPC での ECS on Fargate 検証作業の流れ
ECS on Fargate の検証作業の大まかな流れは次のとおりです。
- 必要な VPC エンドポイントの作成
- Elastic Container Registry (ECR) レジストリの作成と VPC エンドポイント経由以外でのプッシュを禁止
- ローカル環境で Docker イメージをビルドして S3 へアップロード
- 閉域 VPC のプライベートサブネットに作業用 EC2 を立てて S3 の VPC エンドポイント経由で Docker イメージをコピーしてロード
- ECR の VPC エンドポイント経由で Docker イメージをプッシュ
- ECS でプライベートサブネットに Fargate タスクを起動
Auto Scaling や ELB の設定は今回しないことにします。
なお、環境は全て東京リージョン (ap-northeast-1) に作ることとします。
ECR と ECS 用の VPC エンドポイントを作成
今回 ECS は Fargate 起動タイプで実行します。
閉域 VPC から ECR へ Docker イメージを Push できるようにするため、ECR へのインターフェース型の VPC エンドポイントを作成します。作成するエンドポイントは次のとおりです。
- com.amazonaws.ap-northeast-1.ecr.dkr
- com.amazonaws.ap-northeast-1.ecr.api
- com.amazonaws.ap-northeast-1.logs(ログに CloudWatch Logs を使う場合)
EC2 で ECS を実行する場合は、この他に ECS 関係の VPC エンドポイントが必要なようです。
その他に S3 への VPC エンドポイントも必要なので作成します。ゲートウェイ型エンドポイントにしておくとコストがかからずおすすめです。
検証用途のため、インターフェース型エンドポイントは作業終了後にまとめて消せるように、CloudFormation のスタックにしておきます。参考にテンプレートを載せます。
AWSTemplateFormatVersion: 2010-09-09
Description: CloudFormation Template for Create VPC Endpoint
Parameters:
TargetVpcId:
Description: Enter VPC ID
Type: String
Default: 'デフォルトの VPC ID'
TargetSubnetId:
Description: Enter Subnet ID
Type: String
Default: 'デフォルトのサブネット ID'
Resources:
EcrDkrInterfaceEndpoint:
Type: 'AWS::EC2::VPCEndpoint'
Properties:
VpcEndpointType: 'Interface'
ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ecr.dkr'
VpcId: !Ref TargetVpcId
PrivateDnsEnabled: true
SubnetIds:
- !Ref TargetSubnetId
SecurityGroupIds:
- !Ref VpceSecurityGroup
EcrApiInterfaceEndpoint:
Type: 'AWS::EC2::VPCEndpoint'
Properties:
VpcEndpointType: 'Interface'
ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ecr.api'
VpcId: !Ref TargetVpcId
PrivateDnsEnabled: true
SubnetIds:
- !Ref TargetSubnetId
SecurityGroupIds:
- !Ref VpceSecurityGroup
LogInterfaceEndpoint:
Type: 'AWS::EC2::VPCEndpoint'
Properties:
VpcEndpointType: 'Interface'
ServiceName: !Sub 'com.amazonaws.${AWS::Region}.logs'
VpcId: !Ref TargetVpcId
PrivateDnsEnabled: true
SubnetIds:
- !Ref TargetSubnetId
SecurityGroupIds:
- !Ref VpceSecurityGroup
VpceSecurityGroup:
Type: 'AWS::EC2::SecurityGroup'
Properties:
GroupDescription: 'Allow HTTPS traffic from the VPC'
GroupName: test01-dev-sg-ecr-ap-vpce
VpcId: !Ref TargetVpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 10.128.128.0/20
ECR にリポジトリを作成する
ECR のマネジメントコンソールから ECR のプライベートレジストリにリポジトリを作成します。ここではリポジトリ名は「test01/repo-keycloak」としました。
この時、プライベートレジストリのアクセス許可に特定の VPC エンドポイントからのみのアクセスを許可する設定をすることで、インターネット経由でのプライベートレジストリへのアクセスを禁止します。
ECR のマネジメントコンソールから「プライベートレジストリ」→「許可」に進み、以下のような JSON を編集します。
{
"Sid": "DenyExceptSpecificVPCEndpoint",
"Effect": "Deny",
"Principal": "*",
"Action": "ecr:*",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:SourceVpce": [
"com.amazonaws.ap-northeast-1.ecr.api の VPC エンドポイント ID",
"com.amazonaws.ap-northeast-1.ecr.dkr の VPC エンドポイント ID"
]
}
}
}
キャプチャの例では保守用に特定の IAM ロールの場合の除外設定を入れていますが気にしないでください。
検証用の Docker イメージを S3 へアップロードする
検証用の Docker イメージなので何でもいいのですが、今回は認証認可サーバーとしてよく使われる Keycloak のイメージを使うことにしました。
インターネット接続可能なローカルの環境にて Keycloack 公式サイト の手順でローカルに Keycloak の Docker コンテナを実行します。今回 Mac で実行したので、アーキテクチャが Linux/ARM64 になることを意識しておきます。
$ docker run -p 127.0.0.1:8080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:26.3.4 start-dev
一旦コンテナを停止して、Docker イメージを tar にアーカイブし、s3 へアップロードします。
$ docker save 'イメージ ID' > keycloak.26.3.4.tar
$ aws s3 cp keycloak.26.3.4.tar s3://'アップロード先の S3 バケット' --profile 'プロファイル名'
ここからは基本的にインターネット接続のできない閉域 VPC にて作業していきます。
作業用 EC2 の設定
閉域 VPC から ECR へ Docker イメージをプッシュするため、この作業は VPC 内の EC2 インスタンスから実施します。
閉域 VPC のプライベートサブネットに EC2 インスタンスを起動します。ここではアーキテクチャが Graviton の Amazon Linux 2023 でインスタンスを起動しました。
IAM ロールのアタッチ
このインスタンスには、S3 にアクセス可能で、以下の ECR のアクションが許可されたポリシーを割り当てた IAM ロールをアタッチしておきます。
- ecr:GetAuthorizationToken
- ecr:InitiateLayerUpload
- ecr:UploadLayerPart
- ecr:CompleteLayerUpload
- ecr:ListImages
- ecr:PutImage
- ecr:DescribeImages
- ecr:BatchCheckLayerAvailability
ecr:BatchCheckLayerAvailability については、AWS CLI から ECR へイメージをプッシュするには不要という情報があったのですが、私の環境ではこれを追加しないと 'error parsing HTTP 403 response body: unexpected end of JSON input: ""' となってしまい、プッシュが成功しませんでした。
Docker のインストール
作業用の EC2 インスタンスに、Docker をインストールします。Amazon Linux 2023 だと次のとおりです。なお、このインスタンスはプライベートサブネットにあるためインターネット接続ができませんが、事前に S3 の VPC エンドポイントを作成すれば dnf からパッケージのダウンロードが可能です。
$ sudo dnf update
$ sudo dnf install docker
$ sudo systemctl start docker
$ sudo systemctl enable docker
$ sudo usermod -aG docker ec2-user
Docker イメージのロード
S3 から先ほどアップロードした tar ファイルになっている Docker イメージを S3 の VPC エンドポイント経由でダウンロードし、Docker にロードします。
$ aws s3 cp s3://'アップロード先の S3 バケット' .
$ docker load < keycloak.26.3.4.tar
Docker イメージを ECR へプッシュする
Docker イメージの ECR リポジトリへのプッシュは、マネジメントコンソールにサンプルのコマンドが表示されますので、これを参考に作業します。
まずは Keycloak の Docker イメージの名前を ECR に作成したリポジトリと合わせて変更します。
$ docker tag 'Docker イメージの ID' 'AWS アカウント ID'.dkr.ecr.ap-northeast-1.amazonaws.com/'リポジトリ名':latest
作業用 EC2 インスタンスの Docker クライアントを VPC エンドポイント経由で ECR に認証します。AWS CLI から次のコマンドを実行します。
$ aws ecr get-login-password | docker login --username AWS --password-stdin 'AWS アカウント ID'.dkr.ecr.ap-northeast-1.amazonaws.com
ログインに成功したら、Docker イメージを ECR リポジトリにプッシュします。
$ docker push 'AWS アカウント ID'.dkr.ecr.ap-northeast-1.amazonaws.com/test01/repo-keycloak:latest
ここでエラーが出る場合は VPC エンドポイントが足りてないか、IAM ロールに権限が不足しているかの可能性が高いと思います。(何度も嵌りました)
Docker イメージを ECR のリポジトリへプッシュできたら、次は ECS の設定です。
ECS on Fargate の設定
ECS クラスターの作成
ECS のマネジメントコンソールからクラスターを作成します。インフラストラクチャは「AWS Fargate」を選びます。
実はマネジメントコンソールからクラスターを作成すると、初回は以下のキャプチャのようなエラーが出ます。これはクラスター作成時に裏で CloudFormation のスタックが動いており、この中でクラスターの作成に必要な IAM ロール (AWSServiceRoleForECS) が作られるのですが、これが無いために失敗しているようです。
この場合、CloudFormation のマネジメントコンソールからエラーとなっているスタックを削除してから、再度クラスターを作成することで今度は IAM ロールが作成済みのため、クラスターの作成に成功します。
ECS タスク定義の作成
次にタスク定義を作成します。「起動タイプ」は「AWS Fargate」、「オペレーティングシステム/アーキテクチャ」は「Linux/ARM64」とします。
「タスク実行ロール」は、自動作成してもらうことにします。事前に適切な IAM ロールを作成済みであればそれを選択します。
次にコンテナの設定です。「イメージ URI」には ECR にプッシュ済みの Docker イメージを選択します。Keycloak のコンテナは TCP:8080 でサービスを待ち受けているので、ポートマッピングをそのように設定します。
環境変数には Keycloack 公式サイト を参考に、「KC_BOOTSTRAP_ADMIN_USERNAME」「KC_BOOTSTRAP_ADMIN_PASSWORD」の値を渡します(検証ではなく本番であれば Parameter Store などで値を渡した方が良いと思います。)
ログの設定はデフォルトで CloudWatch になっているのでそのままにしました。
CloudWatch Logs にロググループが作成されます。このため閉域 VPC で ECS を動かす場合は CloudWatch Logs への VPC エンドポイントを作成する必要があるわけです。
コンテナに渡すコマンドを指定します。これも Keycloak 公式サイトのとおり、検証用なので「start-dev」を渡します。
ECS サービスの作成
タスク定義が作成できたら、サービスを作成します。「既存のクラスター」に作成したクラスターを、「起動タイプ」は「FARGATE」を選びます。
コンテナを起動するネットワークを設定します。併せてコンテナに割り当てるセキュリティグループも作ってしまいます。
プライベートサブネット内なのでパブリック IP アドレスの割当てはオフにします。
サービスが作成できました。
サービスにアクセスするためのプライベート IP アドレスを確認します。
確認したプライベート IP アドレスの 8080 番ポートへ、閉域 VPC と接続されたオンプレミスにあるクライアントの Web ブラウザからアクセスしてみます。
閉域 VPC の中でも ECS on Fargate のコンテナで Keycloak が実行できていることが確認できました。
ECS on Fargate を構築してみた感想
Fargate の利点は Docker ホストとなるサーバー自体の管理をマネージドでやってくれるため、管理しなくて済むことです。今回は構築しただけなのでその真価は感じられていないですが、インフラをタスクとして定義できるのは運用がかなり楽になるのではと思いました。
一方、閉域の VPC 内で構築するには、ECR にインターネット経由でアクセスできないようにしたり、VPC エンドポイントをいくつか作成する必要があったり、土台の環境を作るのに若干の面倒さがありました。
今回、Fargate の運用面にはほとんど触れられなかったので、引き続き検証を続けてみたいと思います。
ちなみに Keycloak 自体の構築について興味があれば、以下の記事で少し解説していますので参考にしてみてください。