AWS公式のECSハンズオンがとても良かった!!

# はじめに
お疲れ様です。矢儀 @yuki_ink です。
こちらのAWS公式ハンズオンをやってみました。

https://catalog.workshops.aws/ecs-web-application-handson/ja-JP

ECSとFargate/EC2を利用した環境構築から、CI/CDパイプラインを利用したデプロイまで、一通り体験できる素晴らしいハンズオンでした。

次のようなみなさんにおすすめです。

- ECSを知識として知ってはいるが、実際に触ったことがない
- コンテナの何が優れているのか、実感を持っては理解できない
- CI/CDパイプラインでコンテナをデプロイしてみたい

ハンズオンで構築する環境の構成イメージはこちら。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/b29bb61d-9e08-4ca2-bd03-f3068979195a.png)


:::note
この記事は、ハンズオンで学んだ内容を備忘的に残すことを目的とさせていただいてます。
詳細な手順などはAWSのハンズオンページをご確認ください！
実際に手を動かすことによって得られる学びは非常に大きいので、是非ご自身でもやってみていただければと思います！
:::


# 1. VS Code Serverの構築
このハンズオンでは、開発環境として [Visual Studio Code Server  (VS Code Server)](https://code.visualstudio.com/docs/remote/vscode-server) を利用するとのことで、まず、CloudFormationでVS Code Serverを構築していきます。

ハンズオンページの `Launch Stack` ボタンをクリックして、開いたCloudFormationの画面で `スタックの作成` ボタンを押せば完了なんですが、、
せっかくなので、ちょっと深堀をしてみます。

## 1.1. 実行するCloudFormationテンプレートについて
なかなか壮大なテンプレートですが、、
主な構成要素としてはこんな感じです。

- SSMドキュメント
    - CloudWatchエージェントやAWS CLI、Git、Dockerなどのインストール
    - VS Code Serverのインストールと設定
    - Nginxによるリバースプロキシ設定
- SSMドキュメントを実行するLambda関数
    - cfn-response モジュールで、スタック作成時に自動実行される
- EC2
    - SSMドキュメントの実行先
- CloudFront
    - EC2をオリジンとするディストリビューションとキャッシュ設定
- Secrets Manager シークレット
    - VS Codeのユーザー認証情報（パスワード）を保管

<details>
  <summary>実行するCloudFormationテンプレート</summary>

```code-server.yaml
Description: Create a VS code-server instance with an Amazon CloudFront distribution for use in Workshop Studio. Version 4.0.0
Parameters:
  VSCodeUser:
    Type: String
    Description: UserName for VS code-server
    Default: participant
  InstanceName:
    Type: String
    Description: VS code-server EC2 instance name
    Default: VSCodeServer
  InstanceVolumeSize:
    Type: Number
    Description: VS code-server EC2 instance volume size in GB
    Default: 40
  InstanceType:
    Description: VS code-server EC2 instance type
    Type: String
    Default: t3.medium
    AllowedPattern: '^(t3|t4|c6|c7|m6|m7|m8)[g|i|a]?(d|n|dn|-flex)?\.(nano|micro|small|medium|large|[2|4|9|12|16|18|24|32|48]?xlarge)$'
    ConstraintDescription: Must be a valid t, c or m series EC2 instance type
  InstanceOperatingSystem:
    Description: VS code-server EC2 operating system
    Type: String
    Default: AmazonLinux-2023
    AllowedValues: ['AmazonLinux-2023', 'Ubuntu-22', 'Ubuntu-24']
  HomeFolder:
    Type: String
    Description: Folder to open in VS Code server
    Default: /workshop
  DevServerBasePath:
    Type: String
    Description: Base path for the application to be added to Nginx sites-available list
    Default: app
  DevServerPort:
    Type: Number
    Description: Port for the DevServer
    Default: 8081
  RepoUrl:
    Description: Remote repo URL to clone. To not clone a remote repo, leave blank
    Type: String
    Default: ''
  AssetZipS3Path:
    Description: S3 path holding the asset zip file to be copied into the home folder. To not include any assets, leave blank
    Type: String
    Default: ''
  BranchZipS3Path:
    Description: S3 path holding the branches zip file to be checked into the git repo, with each folder being a branch. The content of each folder will added as under a branch, with the folder name being used as the branch name. To leave the empty, leave blank
    Type: String
    Default: ''
  FolderZipS3Path:
    Description: S3 path holding the folder zip file, with each folder being a subfolder of the home directory. Each folder will have its own local git repo. To not include any folders, leave blank
    Type: String
    Default: ''
Conditions:
  IsAL2023: !Equals [!Ref InstanceOperatingSystem, 'AmazonLinux-2023']
  IsGraviton: !Or
    - !Equals [!Select [0, !Split ['.', !Ref InstanceType]], 't4g']
    - !Equals [!Select [0, !Split ['.', !Ref InstanceType]], 'c6g']
    - !Equals [!Select [0, !Split ['.', !Ref InstanceType]], 'c7g']
    - !Equals [!Select [0, !Split ['.', !Ref InstanceType]], 'c7gd']
    - !Equals [!Select [0, !Split ['.', !Ref InstanceType]], 'c8g']
    - !Equals [!Select [0, !Split ['.', !Ref InstanceType]], 'm6g']
    - !Equals [!Select [0, !Split ['.', !Ref InstanceType]], 'm6gd']
    - !Equals [!Select [0, !Split ['.', !Ref InstanceType]], 'm7g']
    - !Equals [!Select [0, !Split ['.', !Ref InstanceType]], 'm7gd']
    - !Equals [!Select [0, !Split ['.', !Ref InstanceType]], 'm8g']
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Instance Configuration
        Parameters:
          - InstanceName
          - InstanceVolumeSize
          - InstanceType
          - InstanceOperatingSystem
      - Label:
          default: Code Server Configuration
        Parameters:
          - VSCodeUser
          - HomeFolder
          - DevServerBasePath
          - DevServerPort
          - RepoUrl
          - AssetZipS3Path
          - BranchZipS3Path
          - FolderZipS3Path
    ParameterLabels:
      VSCodeUser:
        default: VS code-server user name
      InstanceName:
        default: Instance name
      InstanceVolumeSize:
        default: Instance volume size
      InstanceType:
        default: Instance type
      InstanceOperatingSystem:
        default: Instance operating system
      HomeFolder:
        default: VS code-server home folder
      DevServerBasePath:
        default: Application base path
      DevServerPort:
        default: Application port
      RepoUrl:
        default: Git repo URL
      AssetZipS3Path:
        default: Asset file S3 path
      BranchZipS3Path:
        default: Branch file S3 path
      FolderZipS3Path:
        default: Folder file S3 path
Mappings:
  ArmImage:
    # aws ssm get-parameters-by-path --path "/aws/service/canonical/ubuntu/" --recursive --query "Parameters[*].Name"  > canonical-ami.txt
    # aws ssm get-parameters-by-path --path "/aws/service/ami-amazon-linux-latest/" --recursive --query "Parameters[*].Name"  > amazon-ami.txt
    Ubuntu-22:
      ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/jammy/stable/current/arm64/hvm/ebs-gp2/ami-id}}'
    Ubuntu-24:
      ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/noble/stable/current/arm64/hvm/ebs-gp3/ami-id}}'
    AmazonLinux-2023:
      ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64}}'
  AmdImage:
    Ubuntu-22:
      ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/jammy/stable/current/amd64/hvm/ebs-gp2/ami-id}}'
    Ubuntu-24:
      ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/noble/stable/current/amd64/hvm/ebs-gp3/ami-id}}'
    AmazonLinux-2023:
      ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64}}'
  AWSRegionsPrefixListID:
    # aws ec2 describe-managed-prefix-lists  --region <REGION> | jq -r '.PrefixLists[] | select (.PrefixListName == "com.amazonaws.global.cloudfront.origin-facing") | .PrefixListId'
    ap-northeast-1:
      PrefixList: pl-58a04531
    ap-northeast-2:
      PrefixList: pl-22a6434b
    ap-south-1:
      PrefixList: pl-9aa247f3
    ap-southeast-1:
      PrefixList: pl-31a34658
    ap-southeast-2:
      PrefixList: pl-b8a742d1
    ca-central-1:
      PrefixList: pl-38a64351
    eu-central-1:
      PrefixList: pl-a3a144ca
    eu-north-1:
      PrefixList: pl-fab65393
    eu-west-1:
      PrefixList: pl-4fa04526
    eu-west-2:
      PrefixList: pl-93a247fa
    eu-west-3:
      PrefixList: pl-75b1541c
    sa-east-1:
      PrefixList: pl-5da64334
    us-east-1:
      PrefixList: pl-3b927c52
    us-east-2:
      PrefixList: pl-b6a144df
    us-west-1:
      PrefixList: pl-4ea04527
    us-west-2:
      PrefixList: pl-82a045eb
Resources:
  VSCodeSecret:
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W77
            reason: The default KMS Key used by Secrets Manager is appropriate for this password which will be used to log into VSCodeServer, which has very limited permissions. In addition this secret will not be required to be shared across accounts
    Type: AWS::SecretsManager::Secret
    DeletionPolicy: Delete
    UpdateReplacePolicy: Delete
    Properties:
      Name: !Ref InstanceName
      Description: VS code-server user details
      GenerateSecretString:
        PasswordLength: 16
        SecretStringTemplate: !Sub '{"username":"${VSCodeUser}"}'
        GenerateStringKey: 'password'
        ExcludePunctuation: true
  SecretPlaintextLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: !Sub lambda.${AWS::URLSuffix}
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: AwsSecretsManager
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - secretsmanager:GetSecretValue
                Resource: !Ref VSCodeSecret
  SecretPlaintextLambda:
    Type: AWS::Lambda::Function
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W58
            reason: Warning incorrectly reported. The role associated with the Lambda function has the AWSLambdaBasicExecutionRole managed policy attached, which includes permission to write CloudWatch Logs. See https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AWSLambdaBasicExecutionRole.html
          - id: W89
            reason: CloudFormation custom function does not need the scaffolding of a VPC, to do so would add unnecessary complexity
          - id: W92
            reason: CloudFormation custom function does not need reserved concurrent executions, to do so would add unnecessary complexity
    Properties:
      Description: Return the value of the secret
      Handler: index.lambda_handler
      Runtime: python3.12
      MemorySize: 128
      Timeout: 10
      Architectures:
        - arm64
      Role: !GetAtt SecretPlaintextLambdaRole.Arn
      Code:
        ZipFile: |
          import boto3
          import json
          import cfnresponse
          import logging

          logger = logging.getLogger()
          logger.setLevel(logging.INFO)

          def is_valid_json(json_string):
              logger.debug(f'Calling is_valid_jason:{json_string}')
              try:
                  json.loads(json_string)
                  logger.info('Secret is in json format')
                  return True
              except json.JSONDecodeError:
                  logger.info('Secret is in string format')
                  return False

          def lambda_handler(event, context):
              logger.debug(f'event: {event}')
              logger.debug(f'context: {context}')
              try:
                  if event['RequestType'] == 'Delete':
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData={}, reason='No action to take')
                  else:
                      resource_properties = event['ResourceProperties']
                      secret_name = resource_properties['SecretArn']
                      secrets_mgr = boto3.client('secretsmanager')

                      logger.info('Getting secret from %s', secret_name)

                      secret = secrets_mgr.get_secret_value(SecretId = secret_name)
                      logger.debug(f'secret: {secret}')
                      secret_value = secret['SecretString']

                      responseData = {}
                      if is_valid_json(secret_value):
                          responseData = secret_value
                      else:
                          responseData = {'secret': secret_value}
                      logger.debug(f'responseData: {responseData}')
                      logger.debug(f'type(responseData): {type(responseData)}')
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData=json.loads(responseData), reason='OK', noEcho=True)
              except Exception as e:
                  logger.error(e)
                  cfnresponse.send(event, context, cfnresponse.FAILED, responseData={}, reason=str(e))
  SecretPlaintext:
    Type: Custom::SecretPlaintextLambda
    Properties:
      ServiceToken: !GetAtt SecretPlaintextLambda.Arn
      ServiceTimeout: 15
      SecretArn: !Ref VSCodeSecret
  VSCodeSSMDoc:
    Type: AWS::SSM::Document
    Properties:
      DocumentType: Command
      Content:
        schemaVersion: '2.2'
        description: Bootstrap VS code-server instance
        parameters:
          LinuxFlavor:
            type: String
            default: 'al2023'
          VSCodePassword:
            type: String
            default: !Ref AWS::StackId
        # all mainSteps scripts are in in /var/lib/amazon/ssm/<instanceid>/document/orchestration/<uuid>/<StepName>/_script.sh
        mainSteps:
          - name: InstallCloudWatchAgent
            action: aws:configurePackage
            inputs:
              name: AmazonCloudWatchAgent
              action: Install
          - name: ConfigureCloudWatchAgent
            action: aws:runDocument
            inputs:
              documentType: SSMDocument
              documentPath: AmazonCloudWatch-ManageAgent
              documentParameters:
                action: configure
                mode: ec2
                optionalConfigurationSource: default
                optionalRestart: 'yes'
          - name: InstallAptPackagesApt
            action: aws:runShellScript
            precondition:
              StringEquals:
                - '{{ LinuxFlavor }}'
                - ubuntu
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - dpkg --configure -a
                - apt-get -q update && DEBIAN_FRONTEND=noninteractive apt-get install -y -q apt-utils
                - apt-get -q update && DEBIAN_FRONTEND=noninteractive apt-get install -y -q needrestart unattended-upgrades
                - sed -i 's/#$nrconf{kernelhints} = -1;/$nrconf{kernelhints} = 0;/' /etc/needrestart/needrestart.conf
                - sed -i 's/#$nrconf{verbosity} = 2;/$nrconf{verbosity} = 0;/' /etc/needrestart/needrestart.conf
                - sed -i "s/#\$nrconf{restart} = 'i';/\$nrconf{restart} = 'a';/" /etc/needrestart/needrestart.conf
                - echo "Apt helper packages added. Checking configuration"
                - cat /etc/needrestart/needrestart.conf
          - name: InstallBasePackagesDnf
            action: aws:runShellScript
            precondition:
              StringEquals:
                - '{{ LinuxFlavor }}'
                - al2023
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - dnf install -y --allowerasing curl gnupg whois argon2 unzip nginx openssl
          - name: InstallBasePackagesApt
            action: aws:runShellScript
            precondition:
              StringEquals:
                - '{{ LinuxFlavor }}'
                - ubuntu
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - dpkg --configure -a
                - apt-get -q update && DEBIAN_FRONTEND=noninteractive apt-get install -y -q curl gnupg whois argon2 unzip nginx openssl locales locales-all apt-transport-https ca-certificates software-properties-common
          - name: AddUserDnf
            action: aws:runShellScript
            precondition:
              StringEquals:
                - '{{ LinuxFlavor }}'
                - al2023
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - !Sub |
                  echo 'Adding user: ${VSCodeUser}'
                  adduser -c '' ${VSCodeUser}
                  passwd -l ${VSCodeUser}
                  echo "${VSCodeUser}:{{ VSCodePassword }}" | chpasswd
                  usermod -aG wheel ${VSCodeUser}
                - echo "User added. Checking configuration"
                - !Sub getent passwd ${VSCodeUser}
          - name: AddUserApt
            action: aws:runShellScript
            precondition:
              StringEquals:
                - '{{ LinuxFlavor }}'
                - ubuntu
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - dpkg --configure -a
                - !Sub |
                  if [[ "${VSCodeUser}" == "ubuntu" ]]
                  then
                    echo 'Using existing user: ${VSCodeUser}'
                  else
                    echo 'Adding user: ${VSCodeUser}'
                    adduser --disabled-password --gecos '' ${VSCodeUser}
                    echo "${VSCodeUser}:{{ VSCodePassword }}" | chpasswd
                    usermod -aG sudo ${VSCodeUser}
                  fi
                - !Sub |
                  tee /etc/sudoers.d/91-vscode-user <<EOF
                  ${VSCodeUser} ALL=(ALL) NOPASSWD:ALL
                  EOF
                - !Sub mkdir -p /home/${VSCodeUser} && chown -R ${VSCodeUser}:${VSCodeUser} /home/${VSCodeUser}
                - !Sub mkdir -p /home/${VSCodeUser}/.local/bin && chown -R ${VSCodeUser}:${VSCodeUser} /home/${VSCodeUser}
                - echo "User added. Checking configuration"
                - !Sub getent passwd ${VSCodeUser}
          - name: UpdateProfile
            action: aws:runShellScript
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - echo LANG=en_US.utf-8 >> /etc/environment
                - echo LC_ALL=en_US.UTF-8 >> /etc/environment
                - !Sub echo 'PATH=$PATH:/home/${VSCodeUser}/.local/bin' >> /home/${VSCodeUser}/.bashrc
                - !Sub echo 'export PATH' >> /home/${VSCodeUser}/.bashrc
                - !Sub echo 'export AWS_REGION=${AWS::Region}' >> /home/${VSCodeUser}/.bashrc
                - !Sub echo 'export AWS_ACCOUNTID=${AWS::AccountId}' >> /home/${VSCodeUser}/.bashrc
                - !Sub echo 'export NEXT_TELEMETRY_DISABLED=1' >> /home/${VSCodeUser}/.bashrc
                - !Sub echo "export PS1='\[\033[01;32m\]\u:\[\033[01;34m\]\w\[\033[00m\]\$ '" >> /home/${VSCodeUser}/.bashrc
                - !Sub chown -R ${VSCodeUser}:${VSCodeUser} /home/${VSCodeUser}
          - name: InstallAWSCLI
            action: aws:runShellScript
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - mkdir -p /tmp
                - curl -fsSL https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m).zip -o /tmp/aws-cli.zip
                - !Sub chown -R ${VSCodeUser}:${VSCodeUser} /tmp/aws-cli.zip
                - unzip -q -d /tmp /tmp/aws-cli.zip
                - sudo /tmp/aws/install
                - rm -rf /tmp/aws
                - echo "AWS CLI installed. Checking configuration"
                - aws --version
          - name: InstallGitDnf
            action: aws:runShellScript
            precondition:
              StringEquals:
                - '{{ LinuxFlavor }}'
                - al2023
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - dnf install -y git
                - !Sub sudo -u ${VSCodeUser} git config --global user.email "participant@example.com"
                - !Sub sudo -u ${VSCodeUser} git config --global user.name "Workshop Participant"
                - !Sub sudo -u ${VSCodeUser} git config --global init.defaultBranch "main"
                - echo "Git installed. Checking configuration"
                - git --version
                - !Sub |
                  if true;
                  then
                    dnf install -y python3-pip
                    echo "Installing git-remote-codecommit with pip3"
                    PIP_BREAK_SYSTEM_PACKAGES=1 pip3 install git-remote-codecommit
                  fi
          - name: InstallGitApt
            action: aws:runShellScript
            precondition:
              StringEquals:
                - '{{ LinuxFlavor }}'
                - ubuntu
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - dpkg --configure -a
                - add-apt-repository ppa:git-core/ppa
                - apt-get -q update && DEBIAN_FRONTEND=noninteractive apt-get install -y -q git
                - !Sub sudo -u ${VSCodeUser} git config --global user.email "participant@example.com"
                - !Sub sudo -u ${VSCodeUser} git config --global user.name "Workshop Participant"
                - !Sub sudo -u ${VSCodeUser} git config --global init.defaultBranch "main"
                - echo "Git installed. Checking configuration"
                - git --version
                - !Sub |
                  if true;
                  then
                    apt-get -q update && DEBIAN_FRONTEND=noninteractive apt-get install -y -q python3-pip
                    echo "Installing git-remote-codecommit with pip3"
                    PIP_BREAK_SYSTEM_PACKAGES=1 pip3 install git-remote-codecommit
                  fi
          - name: CloneRepo
            action: aws:runShellScript
            inputs:
              timeoutSeconds: 600
              runCommand:
                - '#!/bin/bash'
                - !Sub |
                  if [[ -z "${RepoUrl}" ]]
                  then
                    echo "No Repo"
                  else
                    mkdir -p ${HomeFolder} && chown -R ${VSCodeUser}:${VSCodeUser} ${HomeFolder}
                    RepoUrlRegion=$(echo "${RepoUrl}" | sed "s/{.Region}/${AWS::Region}/g")
                    sudo -u ${VSCodeUser} git clone $RepoUrlRegion ${HomeFolder}
                    echo "Repo $(RepoUrlRegion) cloned. Checking configuration"
                    ls -la ${HomeFolder}
                    sudo -u ${VSCodeUser} git -C ${HomeFolder} remote -v
                  fi
          - name: DownloadAssets
            action: aws:runShellScript
            inputs:
              timeoutSeconds: 600
              runCommand:
                - '#!/bin/bash'
                - !Sub |
                  if [[ -z "${AssetZipS3Path}" ]]
                  then
                    echo "No assets"
                  else
                    mkdir -p ${HomeFolder} && chown -R ${VSCodeUser}:${VSCodeUser} ${HomeFolder}
                    mkdir -p /tmp
                    aws s3 cp s3://${AssetZipS3Path} /tmp/asset.zip
                    chown -R ${VSCodeUser}:${VSCodeUser} /tmp/asset.zip
                    unzip -o /tmp/asset.zip -d ${HomeFolder}
                    chown -R ${VSCodeUser}:${VSCodeUser} ${HomeFolder}
                    if  [[ -d ${HomeFolder}/.git ]] # Indicates that a repo has been cloned
                    then
                      sudo -u ${VSCodeUser} git -C ${HomeFolder} add .
                      sudo -u ${VSCodeUser} git -C ${HomeFolder} commit -m 'Workshop commit'
                    else
                      sudo -u ${VSCodeUser} git -C ${HomeFolder} init
                      sudo -u ${VSCodeUser} git -C ${HomeFolder} add .
                      sudo -u ${VSCodeUser} git -C ${HomeFolder} commit -m 'Initial commit'
                    fi
                    echo "Assets downloaded. Checking configuration: ${HomeFolder}"
                    ls -la ${HomeFolder}
                    sudo -u ${VSCodeUser} git -C ${HomeFolder} branch
                  fi
          - name: DownloadFolders
            action: aws:runShellScript
            inputs:
              timeoutSeconds: 600
              runCommand:
                - '#!/bin/bash'
                - !Sub |
                  if [[ -z "${FolderZipS3Path}" ]]
                  then
                    echo "No folders"
                  else
                    rm -rf /tmp/folder
                    mkdir -p /tmp/folder && chown -R ${VSCodeUser}:${VSCodeUser} /tmp/folder
                    aws s3 cp s3://${FolderZipS3Path} /tmp/asset-folder.zip
                    chown -R ${VSCodeUser}:${VSCodeUser} /tmp/asset-folder.zip
                    unzip -o /tmp/asset-folder.zip -d /tmp/folder
                    chown -R ${VSCodeUser}:${VSCodeUser} /tmp/folder
                    mkdir -p ${HomeFolder} && chown -R ${VSCodeUser}:${VSCodeUser} ${HomeFolder}
                    cd "${HomeFolder}" && cd ..
                    if [[ $(pwd) ==  "/" ]]
                    then
                      targetRootFolder=""
                    else
                      targetRootFolder=$(pwd)
                      chown -R ${VSCodeUser}:${VSCodeUser} .
                    fi
                    find "/tmp/folder" -maxdepth 1 -mindepth 1 -type d | while read sourceFolder; do
                      folder="$(basename $sourceFolder)"
                      echo "Processing folder: $folder"
                      targetFolder=$targetRootFolder/$folder
                      if [[ $targetRootFolder == "" ]]
                      then
                        mv $sourceFolder /
                      else
                        mv $sourceFolder $targetRootFolder
                      fi
                      chown -R ${VSCodeUser}:${VSCodeUser} $targetFolder
                      sudo -u ${VSCodeUser} git -C $targetFolder init
                      sudo -u ${VSCodeUser} git -C $targetFolder add .
                      sudo -u ${VSCodeUser} git -C $targetFolder commit -m "Initial commit"
                      echo "Folder downloaded. Checking configuration: $targetFolder"
                      ls -la $targetFolder
                    done
                    rm -rf /tmp/folder
                  fi
          - name: DownloadBranches
            action: aws:runShellScript
            inputs:
              timeoutSeconds: 600
              runCommand:
                - '#!/bin/bash'
                - !Sub |
                  if [[ -z "${BranchZipS3Path}" ]]
                  then
                    echo "No branches"
                  else
                    rm -rf /tmp/branch
                    rm -rf /tmp/git
                    mkdir -p /tmp/branch && chown -R ${VSCodeUser}:${VSCodeUser} /tmp/branch
                    mkdir -p /tmp/git && chown -R ${VSCodeUser}:${VSCodeUser} /tmp/git
                    aws s3 cp s3://${BranchZipS3Path} /tmp/asset-branch.zip
                    chown -R ${VSCodeUser}:${VSCodeUser} /tmp/asset-branch.zip
                    unzip -o /tmp/asset-branch.zip -d /tmp/branch
                    chown -R ${VSCodeUser}:${VSCodeUser} /tmp/branch
                    mkdir -p ${HomeFolder} && chown -R ${VSCodeUser}:${VSCodeUser} ${HomeFolder}
                    sudo -u ${VSCodeUser} git -C ${HomeFolder} init
                    mv ${HomeFolder}/.git /tmp/git
                    rm -rf ${HomeFolder}
                    mkdir -p ${HomeFolder} && chown -R ${VSCodeUser}:${VSCodeUser} ${HomeFolder}
                    mv /tmp/git/.git ${HomeFolder}
                    find /tmp/branch -maxdepth 1 -mindepth 1 -type d | while read sourceFolder; do
                      branch="$(basename $sourceFolder)"
                      echo "Processing branch: $branch"
                      sudo -u ${VSCodeUser} git -C ${HomeFolder} checkout -b $branch 2>&1
                      cp -a $sourceFolder/. ${HomeFolder}
                      sudo -u ${VSCodeUser} git -C ${HomeFolder} add .
                      sudo -u ${VSCodeUser} git -C ${HomeFolder} commit -m "Initial commit $branch"
                      mv ${HomeFolder}/.git /tmp/git
                      rm -rf ${HomeFolder}
                      mkdir ${HomeFolder} && chown -R ${VSCodeUser}:${VSCodeUser} ${HomeFolder}
                      mv /tmp/git/.git ${HomeFolder}
                    done
                    sudo -u ${VSCodeUser} git -C ${HomeFolder} checkout main 2>&1
                    sudo -u ${VSCodeUser} git -C ${HomeFolder} restore .
                    rm -rf /tmp/branch
                    rm -rf /tmp/git
                    echo "Branches downloaded. Checking configuration: $HomeFolder"
                    sudo -u ${VSCodeUser} git -C ${HomeFolder} branch
                    ls -la ${HomeFolder}
                  fi
          - name: ConfigureCodeServer
            action: aws:runShellScript
            inputs:
              timeoutSeconds: 600
              runCommand:
                - '#!/bin/bash'
                - !Sub export HOME=/home/${VSCodeUser}
                - curl -fsSL https://code-server.dev/install.sh | bash -s -- 2>&1
                - !Sub systemctl enable --now code-server@${VSCodeUser} 2>&1
                - !Sub |
                  tee /etc/nginx/conf.d/code-server.conf <<EOF
                  server {
                      listen 80;
                      listen [::]:80;
                      # server_name \$\{CloudFrontDistribution.DomainName\};
                      server_name *.cloudfront.net;
                      location / {
                        proxy_pass http://localhost:8080/;
                        proxy_set_header Host \$host;
                        proxy_set_header Upgrade \$http_upgrade;
                        proxy_set_header Connection upgrade;
                        proxy_set_header Accept-Encoding gzip;
                      }
                      location /${DevServerBasePath} {
                        proxy_pass http://localhost:${DevServerPort}/${DevServerBasePath};
                        proxy_set_header Host \$host;
                        proxy_set_header Upgrade \$http_upgrade;
                        proxy_set_header Connection upgrade;
                        proxy_set_header Accept-Encoding gzip;
                      }
                  }
                  EOF
                - !Sub mkdir -p /home/${VSCodeUser}/.config/code-server
                - !Sub |
                  tee /home/${VSCodeUser}/.config/code-server/config.yaml <<EOF
                  cert: false
                  auth: password
                  hashed-password: "$(echo -n {{ VSCodePassword }} | argon2 $(openssl rand -base64 12) -e)"
                  EOF
                - !Sub mkdir -p /home/${VSCodeUser}/.local/share/code-server/User/
                - !Sub touch /home/${VSCodeUser}/.hushlogin
                - !Sub mkdir -p ${HomeFolder} && chown -R ${VSCodeUser}:${VSCodeUser} ${HomeFolder}
                - !Sub |
                  tee /home/${VSCodeUser}/.local/share/code-server/User/settings.json <<EOF
                  {
                    "extensions.autoUpdate": false,
                    "extensions.autoCheckUpdates": false,
                    "telemetry.telemetryLevel": "off",
                    "security.workspace.trust.startupPrompt": "never",
                    "security.workspace.trust.enabled": false,
                    "security.workspace.trust.banner": "never",
                    "security.workspace.trust.emptyWindow": false,
                    "auto-run-command.rules": [
                      {
                        "command": "workbench.action.terminal.new"
                      }
                    ]
                  }
                  EOF
                - !Sub chown -R ${VSCodeUser}:${VSCodeUser} /home/${VSCodeUser}
                - !Sub systemctl restart code-server@${VSCodeUser}
                - systemctl restart nginx
                - !Sub sudo -u ${VSCodeUser} --login code-server --install-extension AmazonWebServices.aws-toolkit-vscode --force
                # - !Sub sudo -u ${VSCodeUser} --login code-server --install-extension AmazonWebServices.amazon-q-vscode --force
                - !Sub sudo -u ${VSCodeUser} --login code-server --install-extension ms-vscode.live-server --force
                - !Sub sudo -u ${VSCodeUser} --login code-server --install-extension synedra.auto-run-command --force
                - !Sub chown -R ${VSCodeUser}:${VSCodeUser} /home/${VSCodeUser}
                - echo "Nginx installed. Checking configuration"
                - nginx -t 2>&1
                - systemctl status nginx
                - echo "CodeServer installed. Checking configuration"
                - code-server -v
                - !Sub systemctl status code-server@${VSCodeUser}
          - name: InstallDockerDnf
            action: aws:runShellScript
            precondition:
              StringEquals:
                - '{{ LinuxFlavor }}'
                - al2023
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - dnf install -y docker
                - Fn::Sub: usermod -aG docker ${VSCodeUser}
                - Fn::Sub: sudo -u ${VSCodeUser} --login code-server --install-extension ms-azuretools.vscode-docker --force
                - systemctl start docker.service
                - echo "Docker installed. Checking configuration"
                - docker --version
                - systemctl status docker.service
                - newgrp docker
                - Fn::Sub: systemctl restart code-server@${VSCodeUser}
          - name: InstallDockerApt
            action: aws:runShellScript
            precondition:
              StringEquals:
                - '{{ LinuxFlavor }}'
                - ubuntu
            inputs:
              timeoutSeconds: 300
              runCommand:
                - '#!/bin/bash'
                - dpkg --configure -a
                - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
                - echo "deb [signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release --codename --short) stable" > /etc/apt/sources.list.d/docker.list
                - apt-get -q update && DEBIAN_FRONTEND=noninteractive apt-get install -y -q docker-ce docker-ce-cli containerd.io
                - Fn::Sub: usermod -aG docker ${VSCodeUser}
                - Fn::Sub: sudo -u ${VSCodeUser} --login code-server --install-extension ms-azuretools.vscode-docker --force
                - systemctl start docker.service
                - echo "Docker installed. Checking configuration"
                - docker --version
                - systemctl status docker.service
                - newgrp docker
                - Fn::Sub: systemctl restart code-server@${VSCodeUser}
  # Install optional packages here - any of these blocks can be deleted if that software is not required for the workshop
  # Install Workshop specific packages here

  SSMDocLambdaRole:
    Type: AWS::IAM::Role
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W11
            reason: The Amazon EC2 ssm:*CommandInvocation API actions do not support resource-level permissions, so you cannot control which individual resources users can view in the console. Therefore, the * wildcard is necessary in the Resource element. See https://docs.aws.amazon.com/service-authorization/latest/reference/list_awssystemsmanager.html
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: !Sub lambda.${AWS::URLSuffix}
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: SSMDocOnEC2
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ssm:SendCommand
                Resource:
                  - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:document/${VSCodeSSMDoc}
                  - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:document/AmazonCloudWatch-ManageAgent
                  - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/${VSCodeInstance}
              - Effect: Allow
                Action:
                  - ssm:ListCommandInvocations
                  - ssm:GetCommandInvocation
                Resource: '*'
  RunSSMDocLambda:
    Type: AWS::Lambda::Function
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W58
            reason: Warning incorrectly reported. The role associated with the Lambda function has the AWSLambdaBasicExecutionRole managed policy attached, which includes permission to write CloudWatch Logs. See https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AWSLambdaBasicExecutionRole.html
          - id: W89
            reason: CloudFormation custom function does not need the scaffolding of a VPC, to do so would add unnecessary complexity
          - id: W92
            reason: CloudFormation custom function does not need reserved concurrent executions, to do so would add unnecessary complexity
    Properties:
      Description: Run SSM document on EC2 instance
      Handler: index.lambda_handler
      Runtime: python3.12
      MemorySize: 128
      Timeout: 600
      Environment:
        Variables:
          RetrySleep: 2900
          AbortTimeRemaining: 3200
      Architectures:
        - arm64
      Role: !GetAtt SSMDocLambdaRole.Arn
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import logging
          import time
          import os

          logger = logging.getLogger()
          logger.setLevel(logging.INFO)

          def lambda_handler(event, context):
              logger.debug(f'event: {event}')
              logger.debug(f'context: {context}')

              if event['RequestType'] != 'Create':
                  cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData={}, reason='No action to take')
              else:
                  sleep_ms = int(os.environ.get('RetrySleep'))
                  abort_time_remaining_ms = int(os.environ.get('AbortTimeRemaining'))
                  resource_properties = event['ResourceProperties']
                  instance_id = resource_properties['InstanceId']
                  document_name = resource_properties['DocumentName']
                  cloudwatch_log_group_name = resource_properties['CloudWatchLogGroupName']

                  logger.info(f'Running SSM Document {document_name} on EC2 instance {instance_id}. Logging to {cloudwatch_log_group_name}')

                  del resource_properties['ServiceToken']
                  if 'ServiceTimeout' in resource_properties:
                      del resource_properties['ServiceTimeout']
                  del resource_properties['InstanceId']
                  del resource_properties['DocumentName']
                  del resource_properties['CloudWatchLogGroupName']
                  if 'PhysicalResourceId' in resource_properties:
                      del resource_properties['PhysicalResourceId']

                  logger.debug(f'resource_properties filtered: {resource_properties}')

                  parameters = {}
                  for key, value in resource_properties.items():
                      parameters[key] = [value]

                  logger.debug(f'parameters: {parameters}')

                  retry = True
                  attempt_no = 0
                  time_remaining_ms = context.get_remaining_time_in_millis()

                  ssm = boto3.client('ssm')

                  while (retry == True):
                      attempt_no += 1
                      logger.info(f'Attempt: {attempt_no}. Time Remaining: {time_remaining_ms/1000}s')
                      try:
                          response = ssm.send_command(
                              InstanceIds = [instance_id],
                              DocumentName = document_name,
                              CloudWatchOutputConfig = {'CloudWatchLogGroupName': cloudwatch_log_group_name, 'CloudWatchOutputEnabled': True},
                              Parameters = parameters
                          )
                          logger.debug(f'response: {response}')
                          command_id = response['Command']['CommandId']
                          responseData = {'CommandId': command_id}
                          cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, reason='OK')
                          retry = False

                      except ssm.exceptions.InvalidInstanceId as e:
                          time_remaining_ms = context.get_remaining_time_in_millis()
                          if (time_remaining_ms > abort_time_remaining_ms):
                              logger.info(f'Instance {instance_id} not ready. Sleeping: {sleep_ms/1000}s')
                              time.sleep(sleep_ms/1000)
                              retry = True
                          else:
                              logger.info(f'Instance {instance_id} not ready, timed out. Time remaining {time_remaining_ms/1000}s < Abort time remaining {abort_time_remaining_ms/1000}s')
                              logger.error(e, exc_info=True)
                              cfnresponse.send(event, context, cfnresponse.FAILED, responseData={}, reason='Timed out. Time remaining: ' + str(time_remaining_ms/1000) + 's < Abort time remaining: ' + str(abort_time_remaining_ms/1000) + 's')
                              retry = False

                      except Exception as e:
                          logger.error(e, exc_info=True)
                          cfnresponse.send(event, context, cfnresponse.FAILED, responseData={}, reason=str(e))
                          retry = False
  RunVSCodeSSMDoc:
    Type: Custom::RunSSMDocLambda
    Properties:
      ServiceToken: !GetAtt RunSSMDocLambda.Arn
      ServiceTimeout: 305
      InstanceId: !Ref VSCodeInstance
      DocumentName: !Ref VSCodeSSMDoc
      CloudWatchLogGroupName: !Sub /aws/ssm/${VSCodeSSMDoc}
      VSCodePassword: !GetAtt SecretPlaintext.password
      LinuxFlavor: !If [IsAL2023, 'al2023', 'ubuntu']
  VSCodeInstanceBootstrapRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - !Sub ec2.${AWS::URLSuffix}
                - !Sub ssm.${AWS::URLSuffix}
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore
        - !Sub arn:${AWS::Partition}:iam::aws:policy/CloudWatchAgentServerPolicy
        - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonQDeveloperAccess
        - !Sub arn:${AWS::Partition}:iam::aws:policy/ReadOnlyAccess
        - !Sub arn:${AWS::Partition}:iam::aws:policy/AWSCodeCommitPowerUser
        - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess
        - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/ElasticLoadBalancingFullAccess
  VSCodeInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref VSCodeInstanceBootstrapRole
  VSCodeInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !If
        - IsGraviton
        - !FindInMap [ArmImage, !Ref InstanceOperatingSystem, ImageId]
        - !FindInMap [AmdImage, !Ref InstanceOperatingSystem, ImageId]
      InstanceType: !Ref InstanceType
      BlockDeviceMappings:
        - DeviceName: !If [IsAL2023, /dev/xvda, /dev/sda1]
          Ebs:
            VolumeSize: !Ref InstanceVolumeSize
            VolumeType: gp3
            DeleteOnTermination: true
            Encrypted: true
      Monitoring: true
      SecurityGroupIds:
        - !Ref SecurityGroup
      IamInstanceProfile: !Ref VSCodeInstanceProfile
      UserData:
        Fn::Base64: !Sub |
          #cloud-config
          hostname: ${InstanceName}
          runcmd:
            - mkdir -p ${HomeFolder} && chown -R ${VSCodeUser}:${VSCodeUser} ${HomeFolder}
      Tags:
        - Key: Name
          Value: !Ref InstanceName
  VSCodeInstanceCachePolicy:
    Type: AWS::CloudFront::CachePolicy
    Properties:
      CachePolicyConfig:
        DefaultTTL: 86400
        MaxTTL: 31536000
        MinTTL: 1
        Name: !Sub
          - ${InstanceName}-${RandomGUID}
          - RandomGUID: !Select [0, !Split ['-', !Select [2, !Split ['/', !Ref 'AWS::StackId']]]]
        ParametersInCacheKeyAndForwardedToOrigin:
          CookiesConfig:
            CookieBehavior: all
          EnableAcceptEncodingGzip: False
          HeadersConfig:
            HeaderBehavior: whitelist
            Headers:
              - Accept-Charset
              - Authorization
              - Origin
              - Accept
              - Referer
              - Host
              - Accept-Language
              - Accept-Encoding
              - Accept-Datetime
          QueryStringsConfig:
            QueryStringBehavior: all
  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W10
            reason: CloudFront Distribution access logging would require setup of an S3 bucket and changes in IAM, which add unnecessary complexity to the template
          - id: W70
            reason: Workshop Studio does not include a domain that can be used to provision a certificate, so it is not possible to setup TLS. See PFR EE-6016
    Properties:
      DistributionConfig:
        Enabled: True
        HttpVersion: http2and3
        CacheBehaviors:
          - AllowedMethods:
              - GET
              - HEAD
              - OPTIONS
              - PUT
              - PATCH
              - POST
              - DELETE
            CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html#managed-cache-policy-caching-disabled
            Compress: False
            OriginRequestPolicyId: 216adef6-5c7f-47e4-b989-5492eafa07d3 # Managed-AllViewer - see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html#:~:text=When%20using%20AWS,47e4%2Db989%2D5492eafa07d3
            TargetOriginId: !Sub CloudFront-${AWS::StackName}
            ViewerProtocolPolicy: allow-all
            PathPattern: '/proxy/*'
        DefaultCacheBehavior:
          AllowedMethods:
            - GET
            - HEAD
            - OPTIONS
            - PUT
            - PATCH
            - POST
            - DELETE
          CachePolicyId: !Ref VSCodeInstanceCachePolicy
          OriginRequestPolicyId: 216adef6-5c7f-47e4-b989-5492eafa07d3 # Managed-AllViewer - see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html#:~:text=When%20using%20AWS,47e4%2Db989%2D5492eafa07d3
          TargetOriginId: !Sub CloudFront-${AWS::StackName}
          ViewerProtocolPolicy: allow-all
        Origins:
          - DomainName: !GetAtt VSCodeInstance.PublicDnsName
            Id: !Sub CloudFront-${AWS::StackName}
            CustomOriginConfig:
              OriginProtocolPolicy: http-only
  SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: F1000
            reason: All outbound traffic should be allowed from this instance. The EC2 instance is provisioned in the default VPC, which already has this egress rule, and it is not possible to duplicate this egress rule in the default VPC
    Properties:
      GroupDescription: SG for VSCodeServer - only allow CloudFront ingress
      SecurityGroupIngress:
        - Description: Allow HTTP from com.amazonaws.global.cloudfront.origin-facing
          IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          SourcePrefixListId: !FindInMap [AWSRegionsPrefixListID, !Ref 'AWS::Region', PrefixList]
  VSCodeHealthCheckLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: !Sub lambda.${AWS::URLSuffix}
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
  VSCodeHealthCheckLambda:
    Type: AWS::Lambda::Function
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W58
            reason: Warning incorrectly reported. The role associated with the Lambda function has the AWSLambdaBasicExecutionRole managed policy attached, which includes permission to write CloudWatch Logs. See https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AWSLambdaBasicExecutionRole.html
          - id: W89
            reason: CloudFormation custom function does not need the scaffolding of a VPC, to do so would add unnecessary complexity
          - id: W92
            reason: CloudFormation custom function does not need reserved concurrent executions, to do so would add unnecessary complexity
    Properties:
      Description: Run health check on VS code-server instance
      Handler: index.lambda_handler
      Runtime: python3.12
      MemorySize: 128
      Timeout: 600
      Environment:
        Variables:
          RetrySleep: 2900
          AbortTimeRemaining: 5000
      Architectures:
        - arm64
      Role: !GetAtt VSCodeHealthCheckLambdaRole.Arn
      Code:
        ZipFile: |
          import json
          import cfnresponse
          import logging
          import time
          import os
          import http.client
          from urllib.parse import urlparse

          logger = logging.getLogger()
          logger.setLevel(logging.INFO)

          def healthURLOk(url):
              # Using try block to catch connection errors and JSON conversion errors
              try:
                  logger.debug(f'url: {url}')
                  parsed_url = urlparse(url)
                  if parsed_url.scheme == 'https':
                      logger.debug(f'Trying https: {parsed_url.netloc}. Parsed_url: {parsed_url}')
                      conn = http.client.HTTPSConnection(parsed_url.netloc)
                  else:
                      logger.debug(f'Trying http: {parsed_url.netloc}. Parsed_url: {parsed_url}')
                      conn = http.client.HTTPConnection(parsed_url.netloc)
                  conn.request("GET", parsed_url.path or "/")
                  response = conn.getresponse()
                  logger.debug(f'response: {response}')
                  logger.debug(f'response.status: {response.status}')
                  content = response.read()
                  logger.debug(f'content: {content}')
                  # This will be true for any return code below 4xx (so 3xx and 2xx)
                  if 200 <= response.status < 400:
                      response_dict = json.loads(content.decode('utf-8'))
                      logger.debug(f'response_dict: {response_dict}')
                      # Checking for expected keys and if the key has the expected value
                      if 'status' in response_dict and (response_dict['status'].lower() == 'alive' or response_dict['status'].lower() == 'expired'):
                          # Response code 200 and correct JSON returned
                          logger.info(f'Health check OK. Status: {response_dict['status'].lower()}')
                          return True
                      else:
                          # Response code 200 but the 'status' key is either not present or does not have the value 'alive' or 'expired'
                          logger.info(f'Health check failed. Status: {response_dict['status'].lower()}')
                          return False
                  else:
                      # Response was not ok (error 4xx or 5xx)
                      logger.info(f'Healthcheck failed. Return code: {response.status}')
                      return False

              except http.client.HTTPException as e:
                  # URL malformed or endpoint not ready yet, this should only happen if we can not DNS resolve the URL
                  logger.error(e, exc_info=True)
                  logger.error(f'Healthcheck failed: HTTP Exception. URL invalid and/or endpoint not ready yet')
                  return False

              except json.decoder.JSONDecodeError as e:
                  # The response we got was not a properly formatted JSON
                  logger.error(e, exc_info=True)
                  logger.info(f'Healthcheck failed: Did not get JSON object from URL as expected')
                  return False

              except Exception as e:
                  logger.error(e, exc_info=True)
                  logger.info(f'Healthcheck failed: General error')
                  return False

              finally:
                  if 'conn' in locals():
                      conn.close()

          def is_valid_json(json_string):
              try:
                  json.loads(json_string)
                  return True
              except ValueError:
                  return False

          def lambda_handler(event, context):
              logger.debug(f'event: {event}')
              logger.debug(f'context: {context}')
              try:
                  if event['RequestType'] != 'Create':
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData={}, reason='No action to take')
                  else:
                      sleep_ms = int(os.environ.get('RetrySleep'))
                      abort_time_remaining_ms = int(os.environ.get('AbortTimeRemaining'))
                      resource_properties = event['ResourceProperties']
                      url = resource_properties['Url']

                      logger.info(f'Testing url: {url}')

                      time_remaining_ms = context.get_remaining_time_in_millis()
                      attempt_no = 0
                      health_check = False
                      while (attempt_no == 0 or (time_remaining_ms > abort_time_remaining_ms and not health_check)):
                          attempt_no += 1
                          logger.info(f'Attempt: {attempt_no}. Time Remaining: {time_remaining_ms/1000}s')
                          health_check = healthURLOk(url)
                          if not health_check:
                              logger.debug(f'Healthcheck failed. Sleeping: {sleep_ms/1000}s')
                              time.sleep(sleep_ms/1000)
                          time_remaining_ms = context.get_remaining_time_in_millis()
                      if health_check:
                          logger.info(f'Health check successful. Attempts: {attempt_no}. Time Remaining: {time_remaining_ms/1000}s')
                          cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData={}, reason='VS code-server healthcheck successful')
                      else:
                          logger.info(f'Health check failed. Timed out. Attempts: {attempt_no}. Time remaining {time_remaining_ms/1000}s < Abort time remaining {abort_time_remaining_ms/1000}s')
                          cfnresponse.send(event, context, cfnresponse.FAILED, responseData={}, reason='VS code-server healthcheck failed. Timed out after ' + str(attempt_no) + ' attempts')
                          logger.info(f'Response sent')

              except Exception as e:
                  logger.error(e, exc_info=True)
                  logger.info(f'Health check failed. General exception')
                  cfnresponse.send(event, context, cfnresponse.FAILED, responseData={}, reason=str(e))
  Healthcheck:
    Type: Custom::VSCodeHealthCheckLambda
    Properties:
      ServiceToken: !GetAtt VSCodeHealthCheckLambda.Arn
      ServiceTimeout: 610
      Url: !Sub https://${CloudFrontDistribution.DomainName}/healthz
  CheckSSMDocLambda:
    Type: AWS::Lambda::Function
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W58
            reason: Warning incorrectly reported. The role associated with the Lambda function has the AWSLambdaBasicExecutionRole managed policy attached, which includes permission to write CloudWatch Logs. See https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AWSLambdaBasicExecutionRole.html
          - id: W89
            reason: CloudFormation custom function does not need the scaffolding of a VPC, to do so would add unnecessary complexity
          - id: W92
            reason: CloudFormation custom function does not need reserved concurrent executions, to do so would add unnecessary complexity
    Properties:
      Description: Check SSM document on EC2 instance
      Handler: index.lambda_handler
      Runtime: python3.12
      MemorySize: 128
      Timeout: 600
      Environment:
        Variables:
          RetrySleep: 2900
          AbortTimeRemaining: 5000
      Architectures:
        - arm64
      Role: !GetAtt SSMDocLambdaRole.Arn
      Code:
        ZipFile: "import boto3\nimport cfnresponse\nimport logging\nimport time\nimport os\n\nlogger = logging.getLogger()\nlogger.setLevel(logging.INFO)\n\ndef lambda_handler(event, context):\n    logger.debug(f'event: {event}')\n    logger.debug(f'context: {context}')\n\n    if event['RequestType'] != 'Create':\n        cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData={}, reason='No action to take')\n    else:\n        sleep_ms = int(os.environ.get('RetrySleep'))\n        abort_time_remaining_ms = int(os.environ.get('AbortTimeRemaining'))\n        resource_properties = event['ResourceProperties']\n        instance_id = resource_properties['InstanceId']\n        document_name = resource_properties['DocumentName']\n\n        logger.info(f'Checking SSM Document {document_name} on EC2 instance {instance_id}')\n\n        retry = True\n        attempt_no = 0\n        time_remaining_ms = context.get_remaining_time_in_millis()\n\n        ssm = boto3.client('ssm')\n\n        while (retry == True):\n            attempt_no += 1\n            logger.info(f'Attempt: {attempt_no}. Time Remaining: {time_remaining_ms/1000}s')\n            try:\n                # check to see if document has completed running on instance\n                response = ssm.list_command_invocations(\n                    InstanceId=instance_id,\n                    Details=True\n                )\n                logger.debug(f'Response: {response}')\n                for invocation in response['CommandInvocations']:\n                    if invocation['DocumentName'] == document_name:\n                        invocation_status = invocation['Status']\n                        if invocation_status == 'Success':\n                            logger.info(f'SSM Document {document_name} on EC2 instance {instance_id} complete. Status: {invocation_status}')                                  \n                            cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData={}, reason='OK')\n                            retry = False\n                        elif invocation_status == 'Failed' or invocation_status == 'Cancelled' or invocation_status == 'TimedOut':\n                            logger.info(f'SSM Document {document_name} on EC2 instance {instance_id} failed. Status: {invocation_status}')\n                            reason = ''\n                            # Get information on step that failed, otherwise it's cancelled or timeout\n                            for step in invocation['CommandPlugins']:\n                                step_name = step['Name']\n                                step_status = step['Status']\n                                step_output = step['Output']\n                                logger.debug(f'Step {step_name} {step_status}: {step_output}')\n                                if step_status != 'Success':\n                                    try:\n                                        response_step = ssm.get_command_invocation(\n                                            CommandId=invocation['CommandId'],\n                                            InstanceId=instance_id,\n                                            PluginName=step_name\n                                        )\n                                        logger.debug(f'Step details: {response_step}')\n                                        step_output = response_step['StandardErrorContent']\n                                    except Exception as e:\n                                        logger.error(e, exc_info=True)\n                                    logger.info(f'Step {step_name} {step_status}: {step_output}')\n                                    if reason == '':\n                                        reason = f'Step {step_name} {step_status}: {step_output}'\n                                    else:\n                                        reason += f'\\nStep {step_name} {step_status}: {step_output}'\n                            if reason == '':\n                                reason = f'SSM Document {document_name} on EC2 instance {instance_id} failed. Status: {invocation_status}'\n                            logger.info(f'{reason}')\n                            cfnresponse.send(event, context, cfnresponse.FAILED, responseData={}, reason=reason)\n                            retry = False\n                        else:\n                            logger.info(f'SSM Document {document_name} on EC2 instance {instance_id} not yet complete. Status: {invocation_status}')\n                            retry = True\n                if retry == True:\n                    if (time_remaining_ms > abort_time_remaining_ms):\n                        logger.info(f'Sleeping: {sleep_ms/1000}s')\n                        time.sleep(sleep_ms/1000)\n                        time_remaining_ms = context.get_remaining_time_in_millis()\n                    else:\n                        logger.info(f'Time remaining {time_remaining_ms/1000}s < Abort time remaining {abort_time_remaining_ms/1000}s')\n                        logger.info(f'Aborting check as time remaining {time_remaining_ms/1000}s < Abort time remaining {abort_time_remaining_ms/1000}s')\n                        cfnresponse.send(event, context, cfnresponse.FAILED, responseData={}, reason='Timed out. Time remaining: ' + str(time_remaining_ms/1000) + 's < Abort time remaining: ' + str(abort_time_remaining_ms/1000) + 's')\n                        retry = False\n            except Exception as e:\n                logger.error(e, exc_info=True)\n                cfnresponse.send(event, context, cfnresponse.FAILED, responseData={}, reason=str(e))\n                retry = False\n"
  CheckVSCodeSSMDoc:
    Type: Custom::CheckSSMDocLambda
    DependsOn: Healthcheck
    Properties:
      ServiceToken: !GetAtt CheckSSMDocLambda.Arn
      ServiceTimeout: 610
      InstanceId: !Ref VSCodeInstance
      DocumentName: !Ref VSCodeSSMDoc
Outputs:
  URL:
    Description: VSCode-Server URL
    Value: !Sub https://${CloudFrontDistribution.DomainName}/?folder=${HomeFolder}
  PasswordURL:
    Description: AWS Secrets Manager URL for VSCode-Server Password
    Value:
      Fn::Sub:
        - https://${AWS::Region}.console.aws.amazon.com/secretsmanager/secret?name=${SecretName}&region=${AWS::Region}
        - SecretName:
            Fn::Sub: ${InstanceName}


```
</details>

CloudFrontのURLを叩くとこのような画面が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/dc484eb5-0a2a-404c-91c0-3213b5106291.png)

Secrets Managerから取得したパスワードを入れると、VS Code Serverの画面に遷移します。

## 1.2. VS Code Serverについて
そもそもVS Code Serverって何？？という状況だったので、調べてみました。

その名の通り、リモートのLinuxサーバや仮想マシン上でコードを編集・実行するためのエンジンのようです。
これを利用することで、ChromeなどのWebブラウザーをVSCodeのフロントエンドとして、VS Codeを利用することができるようになります。


>VS Code Server を使用すると、以下のような新しい方法で VS Code を利用できます：
>
>- SSH サポートが制限されているリモートマシン上で開発を行ったり、Web ベースでのアクセスが必要な場合に使用する。
>- iPad やタブレット、Chromebook のように VS Code デスクトップ版をインストールできないマシンで開発する。
>- すべてのコードをブラウザのサンドボックス内で実行できるという、クライアント側のセキュリティ上の利点を活用する。
>
>（出典）[Visual Studio Code Server - Scenarios](https://code.visualstudio.com/docs/remote/vscode-server#_scenarios)

いい機能やん！と思ったのですが、ハンズオンページでは以下のように案内されていました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/12b6ad79-9cca-4af9-b7ae-ecdfbf16ea07.png)

VS Code Serverに何かデメリットがあるのかしらと思い、少し調べてみたところ、以下の記事がヒットしました。

>接続した先の環境上に vscode-server というのをインストールして vscodeの機能を持ったサーバーを起動、そこに接続してなんやかんやするものでした
この vscode-server が曲者でメモリやディスクといったリソースを遠慮なくバカ食いしポートも空いているところを勝手に使用するそう
>
>（出典）[VS Code の Remote Development は仕組みを理解してないと危ない](https://qiita.com/coatly/items/a392925346f091b49279)

おお（　＾ω＾）・・・
まあ、EC2インスタンスを持っておくということだけでも面倒なので、確かに実際の運用を考えると他のサービスを考えるべきなのかもしれません。

| 推奨サービス名                      | 特徴                                                       |
| ---------------------------- | -------------------------------------------------------- |
| SageMaker Studio Code Editor | ブラウザベースの VS Code 環境。IAM 統合、VPC接続、セキュリティ制御が可能。業務利用に適する。   |
| SageMaker Jupyter Lab        | 機械学習用途に特化した統合開発環境。Notebookベースだが、ターミナルやコードエディタも使える。       |
| ローカル環境（VS Code）              | 開発者がローカルマシンにインストールしたVS Codeを使用。Git連携、SSH開発なども可能。構成管理が容易。 |



# 2. コンテナイメージの作成・実行
Dockerの復習です。
Rails アプリケーションのコンテナイメージを作成し、Docker上で実行していきます。

まずは環境の確認。
```
$ docker --version
Docker version 25.0.8, build 0bab007
```

続いてDockerfileを作成します。
```
# ruby:3.2.1 というベースイメージを取得する
FROM public.ecr.aws/docker/library/ruby:3.2.1

# 必要なパッケージ群を取得する
RUN apt-get update -qq && \
    apt-get install -y nodejs postgresql-client npm && \
    rm -rf /var/lib/apt/lists/\*

# ローカルにあるファイルをコンテナイメージ内にコピーする
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile

# Rails アプリケーションを作成する
RUN bundle install && \
    rails new . -O && \
    sed -i -e "52a\  config.hosts.clear\n  config.web_console.allowed_ips = '0.0.0.0/0'\n  config.action_dispatch.default_headers.delete('X-Frame-Options')" config/environments/development.rb

# Rails を 3000 番ポートで起動する
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0", "-p", "3000"]
```
:::note
Dockerfileとは、コンテナイメージの作成手順をコードとして記述したファイルです。
:::

:::note
ここで、ハンズオンページで以下の案内がされています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/01d64bb5-e52d-40fe-a41e-e05a69f625c2.png)

>ここでは、Ruby の Docker 公式イメージを ECR Public からダウンロードするように指定しています。ECR Public を使用することで、Docker Hub の API 制限を気にする必要がなくなります。また、ECR Public に格納されたコンテナイメージは各リージョンに複製され、かつ Amazon CloudFront によってキャッシュされるため、ダウンロード時間の短縮と可用性の向上を実現できます。

なるほど！これはうれしい！積極的にECR Public使おう！
:::

次に、Gemfileを作成します。
```
source 'https://rubygems.org'
gem 'rails', '7.2.2'
```

:::note
Gemfileとは、Rubyアプリケーションで利用するgem（ライブラリ）の一覧を記述するファイルです。

Dockerfile内で `COPY Gemfile /myapp/Gemfile` と記述されており、ビルドの過程で、このファイルを所定の場所にコピーするように想定されています。
:::

コンテナイメージをビルドして、ビルドが完了したらイメージを確認します。
```
$ docker build -t rails-app .

$ docker image ls
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
rails-app    latest    d7f5691a5b22   52 seconds ago   1.43GB
```
:::note
-t のあとの引数では、ビルドするコンテナイメージの "名前" を指定しています。
最後のピリオド (.) は、ビルド時の "コンテクスト" としてカレントディレクトリを指定しています。
ビルドの過程では、指定したコンテクストにあるファイルを参照できます。
:::

コンテナを実行します。
```
docker run -d -p 8081:3000 rails-app:latest
```
:::note
このコマンドでは、ホスト (VS Code Server インスタンス) の 8081 番ポートを、コンテナの 3000 番ポートにマッピングしています。
コンテナの 3000 番ポートでアプリケーションを実行するように Dockerfile を記述したので、ホストの 8081 番ポートにアクセスすることで Rails アプリケーションにアクセスできます。
:::

8081 ポートで実行中の Rails アプリケーションが表示されました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/3a38b7cd-60f9-47cc-a8bb-15bbc5fbc64f.png)



# 3. ECS ハンズオン
いよいよ本題です。

「クラスター」や「タスク」「サービス」といった概念については、事前に復習しておきましょう！
この記事が分かりやすかったです。

https://dev.classmethod.jp/articles/introduction-2024-amazon-ecs/

ハンズオンページでも、このような図で説明がされていました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/ba8a8b16-64ca-44a5-b2ba-8948373e46ff.png)





## 3.1. VPC とサブネットの作成
手順に従い、VPCとパブリックサブネットを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/9c308086-0a59-4d3c-a957-ee89f6a02240.png)


## 3.2. クラスターの作成
手順に従い、ECSクラスターを作成します。

ここでは、インフラストラクチャの設定で、FargateとEC2をどちらも選択しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/27ce5f92-cbb0-4fdf-a1b2-6de997511740.png)

:::note
ECSのインフラストラクチャでFargateを選ぶかEC2を選ぶか、という比較は、以下の記事が参考になると思います。
- [どっちを選ぶ？AWS ECS on EC2とFargateの違い](https://envader.plus/article/441)
- [re:Invent 2024: AWSがECSとFargateの特徴と使い分けを解説](https://zenn.dev/kiiwami/articles/c7df3fbc5185861c)

基本的にFargateでいいかなと！
Fargateを選べないときに、仕方なくEC2を選ぶ、という印象です。
:::

クラスターの作成が完了しました。
今時点では、ECSサービスもタスクも存在していません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/5c2dbea8-c4a3-4bd1-8ca0-56194b1f504d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/8b13a632-9e9a-4ee6-a226-9823163311d6.png)


## 3.3. コンテナイメージの保存
まず、リポジトリを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/1bd71b21-acab-4ba0-ae8f-fed64e20e634.png)

:::note
ハンズオンでは触れられていませんが、 `イメージタグのミュータビリティ` については、こちらの記事が参考になります。
- [ECRはイミュータブルにしておくと安全](https://zenn.dev/levtech/articles/8feb6330f7c767)
- [latestタグ絶対禁止！？ECRでコンテナイメージタグの変更禁止設定がサポートされました！](https://dev.classmethod.jp/articles/ecr-immutable-image-tags/)
:::

:::note
`イメージのスキャン設定 - deprecated` という記載が気になり、調べてみました。
リポジトリを作成したら、自動で「基本スキャン」は有効になっているようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/f4b178ad-0af8-4330-afaf-652374e2da14.png)

（参考）[【アップデート】Amazon ECR ベーシックスキャンが新しくなりました](https://www.sunnycloud.jp/column/%E3%80%90%E3%82%A2%E3%83%83%E3%83%97%E3%83%87%E3%83%BC%E3%83%88%E3%80%91amazon-ecr-%E3%83%99%E3%83%BC%E3%82%B7%E3%83%83%E3%82%AF%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%B3%E3%81%8C%E6%96%B0%E3%81%97%E3%81%8F/)
:::


続いて、先ほど作成したコンテナイメージをリポジトリにプッシュします。
コマンドはECRの画面に全部書いてある！うれしい！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/0e2b5e6b-c850-41d1-8087-607cf75fe3f1.png)

プッシュできました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/73861349-3854-4843-8d0c-8e6ac3a80eec.png)



## 3.4. タスク定義の作成
手順に従い、タスク定義を作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/44929d7a-6e11-407b-96ef-d8d7f9b47690.png)

ECSで1つのタスクが使用するリソース（CPUとメモリ）の大きさを指定する「タスクサイズ」などを入れていきます。

:::note warn
この「タスクサイズ」がFargateの利用料金に直結します。
:::

たくさんパラメータがありますが、それぞれのパラメータの説明は以下の記事にまとまっていました。感謝！

https://blog.serverworks.co.jp/2023/10/24/104221

続いて、ALB 用のセキュリティグループを作成します。
手順では、0.0.0.0/0からのHTTP通信を許可するようなルールにしていますが、、必要に応じてIPレンジを絞ってもいいかもです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/feadf4e9-cfe0-4912-9dc3-8ccb911e21ea.png)


## 3.5. EC2 へのデプロイ
手順に従い、ECSサービスを作成して、EC2にコンテナをデプロイします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/a5dccc32-71c4-40b2-9c0f-95c726fc4ec6.png)

:::note
EC2インスタンスが1台しか動いてないので、AZ冗長ができていないのが分かります。
:::

ECSサービスの設定のなかでFargateかEC2かを選ぶ、ということでしたが、具体的には、この「コンピューティング設定」の中で設定を行うようです。

デフォルトではキャパシティプロバイダーとして、クラスター作成時に自動生成されたAuto Scaling グループが指定されています。
これにより、Auto Scaling グループによって起動されたEC2で、ECSタスク（コンテナ）が動くことになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/8bf6f4cf-5b4d-4dca-9322-196e2b2a78d6.png)

▼自動生成されたAuto Scaling グループ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/1842e9bd-1e3f-46dc-a9bc-8c3e3529105a.png)

サービスの作成が完了すると、クラスターの画面にもそれが反映されていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/c862b955-b2ae-4155-b5f8-9692386bd6d4.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/daf36e6b-db16-4452-99e9-c26cfea403dd.png)


ブラウザからALBにアクセスします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/d2dcd8c5-022d-483b-8d61-951c378f4091.png)
アプリケーションに接続できることが確認できました！


:::note
ハンズオンページの最後に大事なことが書いてありました！
>**Question**
タスクを 3 つ以上に増やしてみるとどうなるでしょうか？ 余力のある方は試してみてください。
>
>**Answer**
いくらタスクを増やしても、現在の設定では最大 2 つまでしかタスクを実行できないことが分かります。
これは、実行環境の t3.medium インスタンスには、ネットワークインターフェースを最大 3 つまでしか追加できないからです。

**インスタンスのプライマリネットワークインターフェイスも1つとしてカウントされるため、通常の設定では、ECSタスクは1つのEC2につき2つまでしか起動できない**、というお話です。

この問題の解決策として「ENI トランキング機能」が提供されています。
詳細は公式ページをご確認ください。
（参考）[Amazon ECS Linux コンテナインスタンスのネットワークインターフェイスを増やす](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/container-instance-eni.html)
:::


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/64b6f917-6316-4543-8781-d6ec11bae686.png)

ということでいよいよFargateへのデプロイを試していきます！

## 3.6. Fargate へのデプロイ
手順に従い、Fargate用のECSサービスを作成して、Faragateにコンテナをデプロイします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/84b40441-5f8d-4ba3-a002-232dba9e1e65.png)


キャパシティプロバイダーとして FARGATE を指定することで、Fargate でタスクを実行するように設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/b0224d94-0e70-4c70-a75d-c53aa042b71d.png)


サービスの作成が完了すると、クラスターの画面にもそれが反映されていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/eae57999-24e0-45af-8edb-44929b73033b.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/6c3e458e-5a76-4950-9435-67a060600e9d.png)


ブラウザからALBにアクセスします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/4c94323b-8611-4df7-810b-76e0fb21d99f.png)
アプリケーションに接続できることが確認できました！

:::note
>Question
タスクを 3 つ以上に増やしてみるとどうなるでしょうか？ 余力のある方は試してみてください。

Fargateなので、タスクを3つ以上起動できます。
実行基盤を意識することなくコンテナを動かすことができる、というのはこういうことか、と理解できた気がします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/84b952fa-8766-4c50-a8a7-463cdcc6af63.png)
:::

## 3.7. オートスケーリングの設定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/a4ede093-eebe-42c2-bc0b-82fa38f07ce5.png)
ということで、Faragate用のサービスの設定を変更していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/9d786b11-789e-4f3e-8f72-e2df4666bc38.png)

手順に従い、heyを使ってオートスケーリングを検証します。
実行基盤側の都合を気にせずスケーリングさせることができるのはいいですね！！



# 4. [発展] CI/CD ハンズオン
AWS Code ファミリーを使用して、ソースコードの変更からコンテナイメージのビルドと push、ECS on Fargate へのデプロイまでの一連の処理を自動化するパイプラインを作成していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/b94fec5f-a21d-4182-8421-e225e87c0151.png)

## 4.1. CodeCommitリポジトリとbuildspec.yamlの作成

まずCodeCommitリポジトリを作成します。
```
# リポジトリの作成
aws codecommit create-repository --repository-name rails-app

# リポジトリを VS Code Server 上の handson ディレクトリと連携
cd /workshop
git init -b main
git remote add origin codecommit::ap-northeast-1://rails-app
```

:::note alert
ここではハンズオン手順に従ってCodeCommitリポジトリを作成していますが、
今までCodeCommitを使っていなかったアカウント(もしくは既存リポジトリが無いアカウント)では、新規リポジトリの作成が不可となっていますので、ご注意ください！

以下の記事を参考に、CodeCommitの代わりにGitHubを利用することをご検討ください。
[CodePipelineによる自動デプロイパイプラインの構築](https://classmethod.github.io/ci-cd-hands-on-ecs/codepipeline/)
:::

続いて、`/workshop` 配下にbuildspec.yamlを作成します。
中身はこちら。
```
version: 0.2

env:
  variables:
    CONTAINER_NAME: "rails-app"

phases:
  pre_build:
    commands:
      # AWS アカウント ID を取得する
      - AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
      # リポジトリの URI を設定する
      - REPOSITORY_URI=${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${CONTAINER_NAME}
      # コミット ID をもとに、コンテナイメージのタグを生成する
      - IMAGE_TAG=$(echo ${CODEBUILD_RESOLVED_SOURCE_VERSION} | cut -c 1-7)
      # ECR にログインする
      - aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com

  build:
    commands:
      # コンテナイメージをビルドする
      - docker build -t ${REPOSITORY_URI}:latest .
      # 生成したイメージタグを付与する
      - docker tag ${REPOSITORY_URI}:latest ${REPOSITORY_URI}:${IMAGE_TAG}

  post_build:
    commands:
      # ビルドしたコンテナイメージを ECR にプッシュする
      - docker push ${REPOSITORY_URI}:${IMAGE_TAG}
      # "コンテナの名前" と "コンテナイメージの URI" を imagedefinitions.json に書き込む
      - printf '[{"name":"%s","imageUri":"%s"}]' ${CONTAINER_NAME} ${REPOSITORY_URI}:${IMAGE_TAG}  > imagedefinitions.json

artifacts:
  files:
    - imagedefinitions.json
```

作成した buildspec.yaml を含めて CodeCommit に push します。

```
git add .
git commit -m "Initial commit"
git push origin main
```


## 4.2. CI/CDパイプラインの作成

手順に従い、パイプラインを作成します。
簡単にパイプラインが作れてしまった。。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/25c2f0ac-b205-4df7-960c-fa146c05cc89.png)

## 4.3. パイプラインの実行

Dockerfileの中身を変更して、CodeCommitにPushします。
```diff
# ruby:3.2.1 というベースイメージを取得する
FROM public.ecr.aws/docker/library/ruby:3.2.1

# 必要なパッケージ群を取得する
RUN apt-get update -qq && \
    apt-get install -y nodejs postgresql-client npm && \
    rm -rf /var/lib/apt/lists/\*

# ローカルにあるファイルをコンテナイメージ内にコピーする
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile

# Rails アプリケーションを作成する
RUN bundle install && \
    rails new . -O && \
    sed -i -e "52a\  config.hosts.clear\n  config.web_console.allowed_ips = '0.0.0.0/0'\n  config.action_dispatch.default_headers.delete('X-Frame-Options')" config/environments/development.rb

+ # welcome ページの背景色を blue にする
+ RUN sed -i -e "s/background-color: #F0E7E9;/background-color: #99CCFF;/g" /usr/local/bundle/gems/railties-7.2.2/lib/rails/templates/rails/welcome/index.html.erb

# Rails を 3000 番ポートで起動する
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0", "-p", "3000"]
```
表示される画面が変わり、デプロイが完了したことが確認できました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/6d54ffd8-5605-4253-a3d5-a5e842116e59.png)


# 5. [発展] Fargate Spot ハンズオン

>Fargate Spot は、Amazon ECS における購入オプションの 1 つです。AWS が持つ空きキャパシティを活用して大幅な割引 (最大 70%) を実現する一方で、リソースの需要が増えた際には、タスクの起動に失敗したり、「2 分前の中断通知」とともにタスクが中断される可能性があります。
>
>今回は、Fargate 用のサービスと Fargate Spot 用のサービスを併用して、それぞれに同じメトリクスに基づくオートスケーリングを設定します。このとき「Fargate Spot の方が早くスケールアウトするように設定する」ことで、「できる限り Fargate Spot を利用して条件を満たそうとしつつ、それができない場合は通常の Fargate を利用する」構成を目指します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/4519bd92-be28-4407-aa3a-189ab712abe2.png)

:::note
工事中！
:::



:::note warn
お片付けは忘れずにやりましょう！
:::






# 終わりに

以上、ECSハンズオンをやってきました！
ハンズオンをやる前はECSの触りの部分しかわかっていませんでしたが、このハンズオンで理解が深まりました。
どの機能で何をコントロールするのか、具体的なイメージが得られたのは非常に大きな収穫でした。
皆様も是非お試しください！

以下、参考にさせていただいた記事です。

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task-maintenance.html

https://tech.nri-net.com/entry/automatic_application_of_fargate_maintenance


最後までお目通しいただき、ありがとうございました。