Docker Compose CLI を使い、docker-compose.yml の設定から Keycloak を AWS ECS にデプロイします。
Docker Compose CLI
https://github.com/docker/compose-cli
従来の docker-compose とは別に、クラウドデプロイ対応の Docker Compose CLI が Docker の CLI プラグインとして提供されています。
インストール
Windows、macOS では Docker Desktop に同梱されています。
Linux では、リポジトリにあるインストールスクリプトを実行します。
docker-ce (コミュニティ版 Docker 環境) はインストール済みとします。
$ curl -L https://raw.githubusercontent.com/docker/compose-cli/main/scripts/install/install_linux.sh | sh
手動でインストールする場合は以下のように。
# docker コマンドをインストールするので、docker-ce で提供されている docker コマンドとは別の場所に配置することになる。 # /usr/local/bin にパスを通した上で /usr/local/bin にインストール $ curl -L -o /usr/local/bin/docker https://github.com/docker/compose-cli/releases/download/v1.0.17/docker-linux-amd64 $ chmod +x /usr/local/bin/docker # docker-ce の docker コマンドに別名でリンクを張る $ ln -s /usr/bin/docker /usr/local/bin/com.docker.cli # compose プラグインを配置する $ mkdir -p ~/.docker/cli-plugins $ curl -L -o ~/.docker/cli-plugins/docker-compose https://github.com/docker/compose-cli/releases/download/v2.0.0-beta.3/docker-compose-linux-amd64 $ chmod +x ~/.docker/cli-plugins/docker-compose
設定
デプロイ用の AWS ユーザを用意しておきます。最低限、以下のロールが必要なようです。
- application-autoscaling:*
- cloudformation:*
- ec2:AuthorizeSecurityGroupIngress
- ec2:CreateSecurityGroup
- ec2:CreateTags
- ec2:DeleteSecurityGroup
- ec2:DescribeRouteTables
- ec2:DescribeSecurityGroups
- ec2:DescribeSubnets
- ec2:DescribeVpcs
- ec2:RevokeSecurityGroupIngress
- ecs:CreateCluster
- ecs:CreateService
- ecs:DeleteCluster
- ecs:DeleteService
- ecs:DeregisterTaskDefinition
- ecs:DescribeClusters
- ecs:DescribeServices
- ecs:DescribeTasks
- ecs:ListAccountSettings
- ecs:ListTasks
- ecs:RegisterTaskDefinition
- ecs:UpdateService
- elasticloadbalancing:*
- iam:AttachRolePolicy
- iam:CreateRole
- iam:DeleteRole
- iam:DetachRolePolicy
- iam:PassRole
- logs:CreateLogGroup
- logs:DeleteLogGroup
- logs:DescribeLogGroups
- logs:FilterLogEvents
- route53:CreateHostedZone
- route53:DeleteHostedZone
- route53:GetHealthCheck
- route53:GetHostedZone
- route53:ListHostedZonesByName
- servicediscovery:*
docker context create
で ECS デプロイ用のコンテキストを作成します。
$ docker context create ecs ecs-test ? Create a Docker context using: [Use arrows to move, type to filter] > An existing AWS profile AWS secret and token credentials AWS environment variables
docker context ls
で確認。
$ docker context ls NAME TYPE DESCRIPTION DOCKER ENDPOINT KUBERNETES ENDPOINT ORCHESTRATOR default moby Current DOCKER_HOST based configuration unix:///var/run/docker.sock swarm ecs-test * ecs
docker context use
でコンテキストを切り替えることで、デプロイ先が ECS になります。
環境変数 DOCKER_CONTEXT
で指定することもできるので、運用によっては direnv で設定すると便利そうです。
$ docker context use ecs-test ecs-test # または $ export DOCKER_CONTEXT=ecs-test # デフォルト (ローカルへのデプロイ) に戻すには $ docker context use default $ unset DOCKER_CONTEXT
デプロイ
docker-compose.yml を用意して、 docker compose up
でデプロイします。
後述の docker-compose.yml (初版) で実施すると以下のようになりました。
$ docker compose up [+] Running 14/14 ⠿ keycloak CreateComplete 265.0s ⠿ KeycloakTaskExecutionRole CreateComplete 21.0s ⠿ KeycloakTCP8080TargetGroup CreateComplete 1.0s ⠿ Cluster CreateComplete 6.0s ⠿ CloudMap CreateComplete 47.0s ⠿ DefaultNetwork CreateComplete 6.0s ⠿ LogGroup CreateComplete 3.1s ⠿ LoadBalancer CreateComplete 153.0s ⠿ Default8080Ingress CreateComplete 1.0s ⠿ DefaultNetworkIngress CreateComplete 0.0s ⠿ KeycloakTaskDefinition CreateComplete 3.0s ⠿ KeycloakServiceDiscoveryEntry CreateComplete 2.0s ⠿ KeycloakTCP8080Listener CreateComplete 3.0s ⠿ KeycloakService CreateComplete 100.0s
docker-compose.yml の内容が CloudFormation テンプレートに翻訳され、CloudFormation スタックが作成されます。
翻訳後の内容は docker compose convert
で確認できます。
$ docker compose convert AWSTemplateFormatVersion: 2010-09-09 Resources: CloudMap: Properties: Description: Service Map for Docker Compose project keycloak Name: keycloak.local Vpc: vpc-46193423 Type: AWS::ServiceDiscovery::PrivateDnsNamespace ...(略)...
スタックを削除する場合は docker compose down
です。
$ docker compose down [+] Running 14/14 ⠿ keycloak DeleteComplete 455.0s ⠿ Default8080Ingress DeleteComplete 1.1s ⠿ DefaultNetworkIngress DeleteComplete 1.1s ⠿ KeycloakService DeleteComplete 402.1s ⠿ KeycloakTCP8080Listener DeleteComplete 2.0s ⠿ DefaultNetwork DeleteComplete 1.1s ⠿ KeycloakTaskDefinition DeleteComplete 2.0s ⠿ KeycloakServiceDiscoveryEntry DeleteComplete 2.0s ⠿ Cluster DeleteComplete 1.9s ⠿ CloudMap DeleteComplete 47.0s ⠿ LogGroup DeleteComplete 1.0s ⠿ KeycloakTaskExecutionRole DeleteComplete 2.0s ⠿ KeycloakTCP8080TargetGroup DeleteComplete 0.0s ⠿ LoadBalancer DeleteComplete 1.0s
docker-compose.yml
docker-compose.yml の内容について見ていきます。 今回は動作確認のため、データベースはデフォルトの h2 を利用しています。
初版
ローカルデプロイ用に書いた docker-compose.yml をベースに、最低限起動できるようにリソース設定を追加しました。
version: '3.9' services: keycloak: image: quay.io/keycloak/keycloak environment: - KEYCLOAK_USER=admin - KEYCLOAK_PASSWORD=complicated-password - TZ=Asia/Tokyo ports: - "8080:8080" deploy: resources: limits: cpus: "1" memory: 2Gb
docker context convert
の結果は以下のようになりました。
(VPC は作成済みの VPC ID が使われます)
AWSTemplateFormatVersion: 2010-09-09 Resources: CloudMap: Properties: Description: Service Map for Docker Compose project keycloak Name: keycloak.local Vpc: vpc-XXXXXXXX Type: AWS::ServiceDiscovery::PrivateDnsNamespace Cluster: Properties: ClusterName: keycloak Tags: - Key: com.docker.compose.project Value: keycloak Type: AWS::ECS::Cluster Default8080Ingress: Properties: CidrIp: 0.0.0.0/0 Description: keycloak:8080/tcp on default network FromPort: 8080 GroupId: Ref: DefaultNetwork IpProtocol: TCP ToPort: 8080 Type: AWS::EC2::SecurityGroupIngress DefaultNetwork: Properties: GroupDescription: keycloak Security Group for default network Tags: - Key: com.docker.compose.project Value: keycloak - Key: com.docker.compose.network Value: keycloak_default VpcId: vpc-XXXXXXXX Type: AWS::EC2::SecurityGroup DefaultNetworkIngress: Properties: Description: Allow communication within network default GroupId: Ref: DefaultNetwork IpProtocol: "-1" SourceSecurityGroupId: Ref: DefaultNetwork Type: AWS::EC2::SecurityGroupIngress KeycloakService: DependsOn: - KeycloakTCP8080Listener Properties: Cluster: Fn::GetAtt: - Cluster - Arn DeploymentConfiguration: MaximumPercent: 200 MinimumHealthyPercent: 100 DeploymentController: Type: ECS DesiredCount: 1 LaunchType: FARGATE LoadBalancers: - ContainerName: keycloak ContainerPort: 8080 TargetGroupArn: Ref: KeycloakTCP8080TargetGroup NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: ENABLED SecurityGroups: - Ref: DefaultNetwork Subnets: - subnet-2ff5d876 - subnet-42c3896a - subnet-22eafe55 PlatformVersion: 1.4.0 PropagateTags: SERVICE SchedulingStrategy: REPLICA ServiceRegistries: - RegistryArn: Fn::GetAtt: - KeycloakServiceDiscoveryEntry - Arn Tags: - Key: com.docker.compose.project Value: keycloak - Key: com.docker.compose.service Value: keycloak TaskDefinition: Ref: KeycloakTaskDefinition Type: AWS::ECS::Service KeycloakServiceDiscoveryEntry: Properties: Description: '"keycloak" service discovery entry in Cloud Map' DnsConfig: DnsRecords: - TTL: 60 Type: A RoutingPolicy: MULTIVALUE HealthCheckCustomConfig: FailureThreshold: 1 Name: keycloak NamespaceId: Ref: CloudMap Type: AWS::ServiceDiscovery::Service KeycloakTCP8080Listener: Properties: DefaultActions: - ForwardConfig: TargetGroups: - TargetGroupArn: Ref: KeycloakTCP8080TargetGroup Type: forward LoadBalancerArn: Ref: LoadBalancer Port: 8080 Protocol: TCP Type: AWS::ElasticLoadBalancingV2::Listener KeycloakTCP8080TargetGroup: Properties: Port: 8080 Protocol: TCP Tags: - Key: com.docker.compose.project Value: keycloak TargetType: ip VpcId: vpc-XXXXXXXX Type: AWS::ElasticLoadBalancingV2::TargetGroup KeycloakTaskDefinition: Properties: ContainerDefinitions: - Command: - ap-northeast-1.compute.internal - keycloak.local Essential: false Image: docker/ecs-searchdomain-sidecar:1.0 LogConfiguration: LogDriver: awslogs Options: awslogs-group: Ref: LogGroup awslogs-region: Ref: AWS::Region awslogs-stream-prefix: keycloak Name: Keycloak_ResolvConf_InitContainer - DependsOn: - Condition: SUCCESS ContainerName: Keycloak_ResolvConf_InitContainer Environment: - Name: KEYCLOAK_PASSWORD Value: Yke97M9rVbdq9xizVNVF - Name: KEYCLOAK_USER Value: admin - Name: TZ Value: Asia/Tokyo Essential: true Image: quay.io/keycloak/keycloak:latest@sha256:45c02bc75a4d440292761a1c1cfc245cfa1160cc2b665ae3299b336ce2078379 LinuxParameters: {} LogConfiguration: LogDriver: awslogs Options: awslogs-group: Ref: LogGroup awslogs-region: Ref: AWS::Region awslogs-stream-prefix: keycloak Name: keycloak PortMappings: - ContainerPort: 8080 HostPort: 8080 Protocol: tcp Cpu: "1024" ExecutionRoleArn: Ref: KeycloakTaskExecutionRole Family: keycloak-keycloak Memory: "2048" NetworkMode: awsvpc RequiresCompatibilities: - FARGATE Type: AWS::ECS::TaskDefinition KeycloakTaskExecutionRole: Properties: AssumeRolePolicyDocument: Statement: - Action: - sts:AssumeRole Condition: {} Effect: Allow Principal: Service: ecs-tasks.amazonaws.com Version: 2012-10-17 ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly Tags: - Key: com.docker.compose.project Value: keycloak - Key: com.docker.compose.service Value: keycloak Type: AWS::IAM::Role LoadBalancer: Properties: LoadBalancerAttributes: - Key: load_balancing.cross_zone.enabled Value: "true" Scheme: internet-facing Subnets: - subnet-2ff5d876 - subnet-42c3896a - subnet-22eafe55 Tags: - Key: com.docker.compose.project Value: keycloak Type: network Type: AWS::ElasticLoadBalancingV2::LoadBalancer LogGroup: Properties: LogGroupName: /docker-compose/keycloak Type: AWS::Logs::LogGroup
Keycloak が ECS 上にデプロイされ、ネットワークロードバランサー経由でアクセスするという形になります。
ここでの注意点としては、
ports
の設定について、公開ポートは ECS の内部ポート (アプリケーションが待ち受けるポート) と一致させる必要がある。(異なるとdocker compose up
時にエラーになります)- デフォルトのリソース設定 (最小値?) では Keycloak の要求環境を満たせないようで、動作中にクラッシュする。
2版
初版の問題点
初版の設定では、Keycloak (のロードバランサー) へのアクセスは HTTP になっています。認証に使うからには HTTPS にすべきです。
また、Keycloak 自身もグローバル IP 下では HTTP による管理者コンソールへのログインを拒否するため、このままでは Web 上での設定ができません。
現状の Docker Compose CLI では、アプリケーションポートによってロードバランサーの種類 (アプリケーション or ネットワーク) やリスナーの接続プロトコル (TCP or TLS or HTTP or HTTPS) が決定され、素の docker-compose.yml の仕様だけではうまく構成できないようです。
x-aws-cloudformation
Docker Compose CLI では docker-compose.yml の仕様を拡張し、AWS に関する設定ができるようになっています。
x-aws-cloudformation
キーを使い、直接 CloudFormation テンプレートを追加していきます。既存項目については上書きする形になります。
version: '3.9' services: keycloak: image: quay.io/keycloak/keycloak environment: - KEYCLOAK_USER=admin - KEYCLOAK_PASSWORD=complicated-password - PROXY_ADDRESS_FORWARDING=true - TZ=Asia/Tokyo ports: - "8080:8080" deploy: resources: limits: cpus: "1" memory: 2Gb x-aws-cloudformation: Resources: KeycloakTCP8080Listener: Properties: Certificates: - CertificateArn: "arn:aws:acm:ap-northeast-1:XXXXXXXXXXXX:certificate/11111111-2222-3333-4444-555555555555" Port: 443 Protocol: HTTPS KeycloakTCP8080TargetGroup: Properties: Protocol: HTTP LoadBalancer: Properties: LoadBalancerAttributes: [] SecurityGroups: - Ref: LoadBalancerNetwork Type: application LoadBalancerNetwork: Properties: GroupDescription: keycloak Security Group for load balancer Tags: - Key: com.docker.compose.project Value: keycloak - Key: com.docker.compose.network Value: keycloak_loadbalancer SecurityGroupIngress: - CidrIp: 0.0.0.0/0 FromPort: 443 ToPort: 443 IpProtocol: TCP VpcId: Fn::GetAtt: [DefaultNetwork, VpcId] Type: AWS::EC2::SecurityGroup
docker context convert
の結果は以下のようになりました。
AWSTemplateFormatVersion: 2010-09-09 Resources: CloudMap: Properties: Description: Service Map for Docker Compose project keycloak Name: keycloak.local Vpc: vpc-XXXXXXXX Type: AWS::ServiceDiscovery::PrivateDnsNamespace Cluster: Properties: ClusterName: keycloak Tags: - Key: com.docker.compose.project Value: keycloak Type: AWS::ECS::Cluster Default8080Ingress: Properties: CidrIp: 0.0.0.0/0 Description: keycloak:8080/tcp on default network FromPort: 8080 GroupId: Ref: DefaultNetwork IpProtocol: TCP ToPort: 8080 Type: AWS::EC2::SecurityGroupIngress DefaultNetwork: Properties: GroupDescription: keycloak Security Group for default network Tags: - Key: com.docker.compose.project Value: keycloak - Key: com.docker.compose.network Value: keycloak_default VpcId: vpc-XXXXXXXX Type: AWS::EC2::SecurityGroup DefaultNetworkIngress: Properties: Description: Allow communication within network default GroupId: Ref: DefaultNetwork IpProtocol: "-1" SourceSecurityGroupId: Ref: DefaultNetwork Type: AWS::EC2::SecurityGroupIngress KeycloakService: DependsOn: - KeycloakTCP8080Listener Properties: Cluster: Fn::GetAtt: - Cluster - Arn DeploymentConfiguration: MaximumPercent: 200 MinimumHealthyPercent: 100 DeploymentController: Type: ECS DesiredCount: 1 LaunchType: FARGATE LoadBalancers: - ContainerName: keycloak ContainerPort: 8080 TargetGroupArn: Ref: KeycloakTCP8080TargetGroup NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: ENABLED SecurityGroups: - Ref: DefaultNetwork Subnets: - subnet-2ff5d876 - subnet-42c3896a - subnet-22eafe55 PlatformVersion: 1.4.0 PropagateTags: SERVICE SchedulingStrategy: REPLICA ServiceRegistries: - RegistryArn: Fn::GetAtt: - KeycloakServiceDiscoveryEntry - Arn Tags: - Key: com.docker.compose.project Value: keycloak - Key: com.docker.compose.service Value: keycloak TaskDefinition: Ref: KeycloakTaskDefinition Type: AWS::ECS::Service KeycloakServiceDiscoveryEntry: Properties: Description: '"keycloak" service discovery entry in Cloud Map' DnsConfig: DnsRecords: - TTL: 60 Type: A RoutingPolicy: MULTIVALUE HealthCheckCustomConfig: FailureThreshold: 1 Name: keycloak NamespaceId: Ref: CloudMap Type: AWS::ServiceDiscovery::Service KeycloakTCP8080Listener: Properties: DefaultActions: - ForwardConfig: TargetGroups: - TargetGroupArn: Ref: KeycloakTCP8080TargetGroup Type: forward LoadBalancerArn: Ref: LoadBalancer Port: 443 Protocol: HTTPS Certificates: - CertificateArn: arn:aws:acm:ap-northeast-1:XXXXXXXXXXXX:certificate/11111111-2222-3333-4444-555555555555 Type: AWS::ElasticLoadBalancingV2::Listener KeycloakTCP8080TargetGroup: Properties: Port: 8080 Protocol: HTTP Tags: - Key: com.docker.compose.project Value: keycloak TargetType: ip VpcId: vpc-XXXXXXXX Type: AWS::ElasticLoadBalancingV2::TargetGroup KeycloakTaskDefinition: Properties: ContainerDefinitions: - Command: - ap-northeast-1.compute.internal - keycloak.local Essential: false Image: docker/ecs-searchdomain-sidecar:1.0 LogConfiguration: LogDriver: awslogs Options: awslogs-group: Ref: LogGroup awslogs-region: Ref: AWS::Region awslogs-stream-prefix: keycloak Name: Keycloak_ResolvConf_InitContainer - DependsOn: - Condition: SUCCESS ContainerName: Keycloak_ResolvConf_InitContainer Environment: - Name: KEYCLOAK_PASSWORD Value: complicated-password - Name: KEYCLOAK_USER Value: admin - Name: PROXY_ADDRESS_FORWARDING Value: "true" - Name: TZ Value: Asia/Tokyo Essential: true Image: quay.io/keycloak/keycloak:latest@sha256:45c02bc75a4d440292761a1c1cfc245cfa1160cc2b665ae3299b336ce2078379 LinuxParameters: {} LogConfiguration: LogDriver: awslogs Options: awslogs-group: Ref: LogGroup awslogs-region: Ref: AWS::Region awslogs-stream-prefix: keycloak Name: keycloak PortMappings: - ContainerPort: 8080 HostPort: 8080 Protocol: tcp Cpu: "1024" ExecutionRoleArn: Ref: KeycloakTaskExecutionRole Family: keycloak-keycloak Memory: "2048" NetworkMode: awsvpc RequiresCompatibilities: - FARGATE Type: AWS::ECS::TaskDefinition KeycloakTaskExecutionRole: Properties: AssumeRolePolicyDocument: Statement: - Action: - sts:AssumeRole Condition: {} Effect: Allow Principal: Service: ecs-tasks.amazonaws.com Version: 2012-10-17 ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly Tags: - Key: com.docker.compose.project Value: keycloak - Key: com.docker.compose.service Value: keycloak Type: AWS::IAM::Role LoadBalancer: Properties: LoadBalancerAttributes: [] Scheme: internet-facing Subnets: - subnet-2ff5d876 - subnet-42c3896a - subnet-22eafe55 Tags: - Key: com.docker.compose.project Value: keycloak Type: application SecurityGroups: - Ref: LoadBalancerNetwork Type: AWS::ElasticLoadBalancingV2::LoadBalancer LogGroup: Properties: LogGroupName: /docker-compose/keycloak Type: AWS::Logs::LogGroup LoadBalancerNetwork: Properties: GroupDescription: keycloak Security Group for load balancer SecurityGroupIngress: - CidrIp: 0.0.0.0/0 FromPort: 443 IpProtocol: TCP ToPort: 443 Tags: - Key: com.docker.compose.project Value: keycloak - Key: com.docker.compose.network Value: keycloak_loadbalancer VpcId: Fn::GetAtt: - DefaultNetwork - VpcId Type: AWS::EC2::SecurityGroup
動作確認のため、DefaultNetwork については変更せず、ECS 側に直接アクセスできる状態になっています。
注意点
環境変数に PROXY_ADDRESS_FORWARDING=true
を指定する
リバースプロキシやロードバランサー等によって Keycloak の前段で TLS を終端させる場合、Keycloak が URL リダイレクトを正しく行えるようにするため、環境変数に PROXY_ADDRESS_FORWARDING=true
を設定して X-Forwarded-Proto
ヘッダを受ける必要があります。
ロードバランサーは Application LoadBalancer (ALB) を使用する
アプリケーションのポートが 80 や 443 以外の場合、ロードバランサーとしてネットワークロードバランサーが設定されます。
この場合、ロードバランサーは TLS を終端するだけなので X-Forwarded-Proto
が送信されず、Keycloak が正しく動作しません。
明示的にアプリケーションロードバランサーを指定する必要があります。
リスナーのポートとTLS 証明書を設定する
リスナー (KeycloakTCP8080Listener) の設定で外部公開ポートと TLS 証明書を指定できます。
Protocol 項目は HTTPS
にする必要がありますが、これはロードバランサーが ALB でないと設定できません。
(ネットワークロードバランサーでは TCP か TLS のみ)
CloudFormation の関数タグは使えない (?)
現状、Docker Compose CLI は !GetAtt
や !Ref
等のタグ (関数の短縮形構文) を解釈できないようです。そのため、上記の LoadBalancerNetwork.VpcId
では完全名構文で関数 Fn::GetAtt
を指定しています。
まとめ
Docker Compose CLI で Keycloak の CloudFormation スタックを構成し ECS にデプロイしました。
現状では制限もありますが、比較的少ない記述で、使い慣れた docker コマンド一発でデプロイできるというのは便利かと思います。