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 コマンド一発でデプロイできるというのは便利かと思います。