KeycloakをALB+EC2構成で構築してみた

先日、Keycloakの環境を検証する機会があったので、構築手順をまとめました。

本ブログはその備忘録です。

Keycloakとは

KeycloakはOIDC認証等を利用してシングル・サインオンを実現するOSSです。

より詳細に知りたい方は、@ITの記事がわかりやすいので、こちらを御覧ください。

構成図

こんな感じの構成をCloudFormationで作ります。Keycloakは、EC2上のDockerで動かします。

HTTPS接続できるようにするため、ALBをSSL終端にしたいです。
証明書にはACMを利用します。
ACMを作成する場合、DNSにRoute53を利用した方が楽なので、Route53も利用します。

Route53のホストゾーンとACMだけは、マネジメントコンソールから作ります。

構築手順

Route53ホストゾーンの構築

Route53を利用するには、何かしらのドメインを所有している必要があります。 弊社ブログを参考に、無料のドメインを利用してみるのもよいと思います。

作成したホストゾーンのドメイン名はメモしておいてください。のちほどCloudFormationテンプレートで使用します。

ACMの構築

ALBで利用するためのACMを作成します。

ACM構築の詳細は、弊社ブログを御覧ください。

作成したACMのARNはメモしておいてください。のちほどCloudFormationテンプレートで使用します。

CloudFormationでALB+EC2の構築

CloudFormationで残りのAWSリソースを作成します。

以下のボタンをクリックすると、CloudFormationスタックのクイック作成ができます。

launch stack button

作成前にパラメーターとして、以下3つの項目を入力してください。

  • HostedZoneName: Route53で登録したドメイン名。末尾のピリオドは省いてください。
    (例:example.com)
  • CertificateArn: 作成したACMのARN。
    (例:arn:aws:acm:ap-northeast-1:123456789012:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
  • AllowedCidr: ALBに対して接続を許可するネットワークCIDR。
    (例:192.0.2.2/32)

パラメーターを入力したら、一番下までスクロールしてIAMのチェックを入れてスタックを作成します。

CloudFormationのスタックが作成できたら、AWSリソースの構築は完了です。

セッションマネージャーでDockerを動かす

EC2の構築ができたので、KeycloakDockerで動かします。こちらのDockerイメージを利用します。

Dockerを動かすためにEC2へ接続する必要がありますが、今回はセッションマネージャーを利用します。
セッションマネージャーはざっくりいうと、ブラウザ上のシェルでEC2のCLI操作ができる機能です。詳細は弊社ブログを御覧ください。

まずはマネジメントコンソールで、Systems Managerのセッションマネージャーを開いて、セッションの開始をクリックします。

そうすると、接続可能なインスタンスが表示されるので、接続したいインスタンスを選択して、セッションを開始します。

そうすると、ブラウザでEC2インスタンスのシェルを動かすことができます。SSHクライアントいらずです。

セッションマネージャーを利用してEC2へ接続する場合、ログインユーザーは ssm-user になります。ですので、ssm-user ユーザーでDockerが使えるようにコマンドを実行して設定します。

$ sudo usermod -a -G docker ssm-user

コマンドを実行したら、設定を反映させるためにセッションを張り直す必要があります。
1回終了して、もう一度セッションマネージャーで新しいセッションを開始します。

新しいセッションが開始できたら、次のコマンドのパスワードを適当に変更して、Dockerでkeycloakを動かします。

$ PASSWORD="Password@01"
$ docker run -d \
  -p 8080:8080 \
  -e PROXY_ADDRESS_FORWARDING=true \
  -e KEYCLOAK_USER=admin \
  -e KEYCLOAK_PASSWORD=${PASSWORD} \
  jboss/keycloak:7.0.0

しばらくすると、Dockerが起動してKeycloakにログインできるようになります。

Keycloakにログインする

Route53でALBに対して、keycloak. + ドメイン名のエイリアスを作成しているので、そのURLでアクセスすることが可能です。
(ドメイン名がexample.comなら、https://keycloak.example.com/でアクセスできます。)

Docker実行時に設定したパスワードでログインすることができます。

これでKeycloakの検証環境を構築することができました。

終わりに

ALB+EC2(Docker)を使って、HTTPS接続できるKeycloakの検証環境を構築してみました。
今回はKeycloak環境を構築しましたが、セッションマネージャーで実行していたDockerイメージを変えれば別の環境にさっとすげ替えることもできます。
Dockerは検証環境の構築にも便利です。

Keycloakの方は、Cognitoとの連携あたりを検証したいと思います。

【おまけ】CloudFormationテンプレート

AWSTemplateFormatVersion: '2010-09-09'
Description: "keycloak sample"

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Required
        Parameters:
          - HostedZoneName
          - CertificateArn
          - AllowedCidr
      - Label:
          default: Options
        Parameters:
          - CidrPrefix
          - SsmParameterValueawsserviceamiamazonlinuxlatestamzn2
          - TargetGroupPort
          - HostName
          - InstanceType

Parameters:
  HostedZoneName:
    Type: String
    Description: "(Example: example.com)"
  CertificateArn:
    Type: String
    Description: "(Example: arn:aws:acm:ap-northeast-1:123456789012:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)"
  AllowedCidr:
    Type: String
    Description: "(Example: 192.0.2.2/32)"
    AllowedPattern: '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$'
  CidrPrefix:
    Type: String
    Default: 10.35
  SsmParameterValueawsserviceamiamazonlinuxlatestamzn2:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2
  TargetGroupPort:
    Type: String
    Default: 8080
  HostName:
    Type: String
    Default: keycloak
  InstanceType:
    Type: String
    Default: t3.small

Resources:
  Vpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Sub "${CidrPrefix}.0.0/16"
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-vpc"

  Igw:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-igw"

  IgwAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref Igw
      VpcId: !Ref Vpc

  RouteDefault:
    Type: AWS::EC2::Route
    DependsOn: IgwAttachment
    Properties:
      RouteTableId: !Ref RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref Igw

  RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Vpc
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-route-table"

  SubnetFront0:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      CidrBlock: !Sub "${CidrPrefix}.0.0/24"
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-front-0-subnet"
      VpcId: !Ref Vpc

  SubnetFront1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select [ 1, !GetAZs '' ]
      CidrBlock: !Sub "${CidrPrefix}.1.0/24"
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-front-1-subnet"
      VpcId: !Ref Vpc

  SubnetRouteTableAttachmentFront0:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref RouteTable
      SubnetId: !Ref SubnetFront0

  SubnetRouteTableAttachmentFront1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref RouteTable
      SubnetId: !Ref SubnetFront1

  # Security Group for ALB
  SecurityGroupAlb:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub "${AWS::StackName}-alb-sg"
      GroupDescription: for alb
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !Ref AllowedCidr
      VpcId: !Ref Vpc
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-alb-sg"

  # Security Group for Keycloak
  SecurityGroupKeycloak:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub "${AWS::StackName}-keycloak-sg"
      GroupDescription: for keycloak server
      SecurityGroupIngress:
        - SourceSecurityGroupId: !Ref SecurityGroupAlb
          IpProtocol: tcp
          FromPort: !Ref TargetGroupPort
          ToPort: !Ref TargetGroupPort
      VpcId: !Ref Vpc
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-keycloak-sg"

  # ALB
  ApplicationLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: !Sub "${AWS::StackName}-alb"
      Type: application
      Scheme: internet-facing
      IpAddressType: ipv4
      SecurityGroups:
        - !Ref SecurityGroupAlb
      Subnets:
        - !Ref SubnetFront0
        - !Ref SubnetFront1
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-alb"
  ApplicationLoadBalancerListenerHTTPS:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      Certificates:
        - CertificateArn: !Ref CertificateArn
      Port: 443
      Protocol: HTTPS
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref AlbTargetGroupKeycloak
      LoadBalancerArn: !Ref ApplicationLoadBalancer

  # ALB Target group
  AlbTargetGroupKeycloak:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: !Sub "${AWS::StackName}-keycloak-tg"
      Port: !Ref TargetGroupPort
      Protocol: HTTP
      Targets:
        - Id: !Ref KeycloakEc2Instance
      # Health check
      HealthCheckProtocol: HTTP
      HealthyThresholdCount: 3
      HealthCheckIntervalSeconds: 30
      Matcher:
        HttpCode: '200'
      VpcId: !Ref Vpc
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-keycloak-tg"

  # IAM Role for EC2
  Ec2InstanceRoleForSSM:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${AWS::StackName}-ec2-role"
      AssumeRolePolicyDocument:
        {
          "Version": "2012-10-17",
          "Statement": [
          {
            "Action": "sts:AssumeRole",
            "Principal": {
              "Service": "ec2.amazonaws.com"
            },
            "Effect": "Allow",
            "Sid": ""
          }
          ]
        }
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore'
  InstanceProfileForSSM:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref Ec2InstanceRoleForSSM
      InstanceProfileName: !Sub "${AWS::StackName}-instance-profile"
  KeycloakEc2Instance:
    Type: AWS::EC2::Instance
    Properties:
      CreditSpecification:
        CPUCredits: standard
      IamInstanceProfile: !Ref InstanceProfileForSSM
      ImageId: !Ref SsmParameterValueawsserviceamiamazonlinuxlatestamzn2
      InstanceType: !Ref InstanceType
      SecurityGroupIds:
        - !Ref SecurityGroupKeycloak
      SubnetId: !Ref SubnetFront0
      UserData:
        Fn::Base64: |
          #!/bin/sh -ex

          yum update -y
          yum install docker -y
          service docker start
          systemctl enable docker.service
          usermod -a -G docker ec2-user
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-keycloak"

  Route53RecordSetKeycloak:
    Type: AWS::Route53::RecordSet
    Properties:
      AliasTarget:
        DNSName: !Sub
          - dualstack.${DNSName}
          - DNSName: !GetAtt ApplicationLoadBalancer.DNSName
        HostedZoneId: !GetAtt ApplicationLoadBalancer.CanonicalHostedZoneID
      HostedZoneName: !Sub
        - "${HostedZoneName}."
        - {
            HostedZoneName: !Ref HostedZoneName
          }
      Name: !Sub
        - "${HostName}.${HostedZoneName}."
        - {
            HostName: !Ref HostName,
            HostedZoneName: !Ref HostedZoneName
          }
      Type: A