Amazon Web Services (AWS) allows you to deploy to their cloud using their Command Line Interface (CLI). While this is not as infrastructure-focused as CloudFormation (or terraform, or CDK, or pulumi), under the hood, it is the same API being called. A common "getting started" example is static web hosting that can be updated from your CI/CD pipeline. To get started, apply your credentials to the CLI instance:
REGION="us-east-1"
aws configure set aws_access_key_id     "$ACCESS_KEY_ID"
aws configure set aws_secret_access_key "$SECRET_ACCESS_KEY"
aws configure set region                "$REGION" 
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
Now, set the parameters for that static site and CI/CD:
DOMAIN="your-web-site.com"
CICDUSER="your-web-site-deploy-user"
POLICY_NAME="YourWebSiteDeployPolicy"
PROFILE="your-web-site-deploy-profile"
BUCKET_NAME="$DOMAIN-static-hosting-$(date +%s)"
DEFAULT_ROOT_OBJECT="index.html"
Since all websites should use HTTPS, generate a certificate (this verifies with email):
CERT_ARN=$(
  aws acm request-certificate \
    --domain-name "$DOMAIN" \
    --subject-alternative-names "*.$DOMAIN" \
    --validation-method EMAIL \
    --region "$REGION" \
    --query CertificateArn \
    --output text
)
Now set up the S3 bucket
echo "==> Creating (or verifying existence of) S3 bucket: $BUCKET_NAME in $REGION"
# Check if the bucket already exists
if aws s3api head-bucket --bucket "$BUCKET_NAME" 2>/dev/null; then
  echo "Bucket $BUCKET_NAME already exists. Skipping create-bucket."
else
  # Bucket does not exist yet → create it
  if [ "$REGION" = "us-east-1" ]; then
    aws s3api create-bucket \
      --bucket "$BUCKET_NAME"
  else
    aws s3api create-bucket \
      --bucket "$BUCKET_NAME" \
      --create-bucket-configuration LocationConstraint="$REGION"
  fi
  echo "Bucket $BUCKET_NAME created."
fi
# UPDATED: Lock down public access at the bucket level (CloudFront will use OAI fetch objects)
echo "==> Applying public access block to $BUCKET_NAME"
aws s3api put-public-access-block \
  --bucket "$BUCKET_NAME" \
  --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
echo "Public access block applied."
UPDATED: create a CloudFront Origin Access Identity (OAI):
# We use the current UNIX timestamp as a CallerReference to ensure idempotency
CALLER_REF="oai-$DOMAIN-$(date +%s)"
# Create the OAI
OAI_ID=$(aws cloudfront create-cloud-front-origin-access-identity \
  --cloud-front-origin-access-identity-config \
    CallerReference="$CALLER_REF",Comment="OAI for $DOMAIN" \
  --query "CloudFrontOriginAccessIdentity.Id" \
  --output text)
# Some value like E1WVR5QO3X455F
echo "OAI created with ID: $OAI_ID"
# Grab the S3CanonicalUserId for the bucket policy
OAI_S3_CANONICAL_USER=$(aws cloudfront get-cloud-front-origin-access-identity \
  --id "$OAI_ID" \
  --query "CloudFrontOriginAccessIdentity.S3CanonicalUserId" \
  --output text)
# Some value like 2aab88cf5dd3b744b75d2bac01085d01fce0d1600d22436a0afcbf8201d9c5eeb63eb5a822a509164fda91a8e78915ed
echo "Retrieved OAI S3CanonicalUserId: $OAI_S3_CANONICAL_USER" 
Attach a bucket policy granting the OAI read access:
echo "==> Attaching bucket policy to allow CloudFront OAI to get objects"
read -r -d '' BUCKET_POLICY_JSON <l<lEOF
{
  "Version": "2008-10-17",
  "Statement": [
    {
      "Sid": "GrantCloudFrontOAIReadAccess",
      "Effect": "Allow",
      "Principal": {
        "CanonicalUser": "$OAI_S3_CANONICAL_USER"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::$BUCKET_NAME/*"
    }
  ]
}
EOF
aws s3api put-bucket-policy \
  --bucket "$BUCKET_NAME" \
  --policy "$BUCKET_POLICY_JSON"
echo "Bucket policy attached."
Create the CloudFront distribution with non-proxy S3 origin (UPDATED using the OAI)
echo "==> Generating CloudFront distribution configuration JSON"
read -r -d '' DIST_CONFIG <<EOF
{
  "CallerReference": "$(date +%s)-$DOMAIN",
  "PriceClass": "PriceClass_100",
  "Aliases": {
    "Quantity": 2,
    "Items": ["$DOMAIN", "www.$DOMAIN"]
  },
  "DefaultRootObject": "$DEFAULT_ROOT_OBJECT",
  "Origins": {
    "Quantity": 1,
    "Items": [{
      "Id": "S3-$BUCKET_NAME",
      "DomainName": "$BUCKET_NAME.s3.amazonaws.com",
      "S3OriginConfig": {
        "OriginAccessIdentity": "origin-access-identity/cloudfront/$OAI_ID"
      }
    }]
  },
  "DefaultCacheBehavior": {
    "TargetOriginId": "S3-$BUCKET_NAME",
    "ViewerProtocolPolicy": "redirect-to-https",
    "AllowedMethods": {
      "Quantity": 2,
      "Items": ["GET","HEAD"]
    },
    "Compress": true,
    "ForwardedValues": {
      "QueryString": false,
      "Cookies": {"Forward": "none"}
    },
    "TrustedSigners": {"Enabled": false,"Quantity": 0},
    "TrustedKeyGroups": {"Enabled": false,"Quantity": 0},
    "MinTTL": 0
  },
  "Comment": "CloudFront distribution for $DOMAIN",
  "Enabled": true,
  "ViewerCertificate": {
    "ACMCertificateArn": "$CERT_ARN",
    "SSLSupportMethod": "sni-only",
    "MinimumProtocolVersion": "TLSv1.2_2018"
  },
  "Restrictions": {
    "GeoRestriction": {"RestrictionType": "none","Quantity": 0}
  }
}
EOF
echo "==> Creating CloudFront distribution ..."
DIST_ID=$(aws cloudfront create-distribution \
  --distribution-config "$DIST_CONFIG" \
  --query "Distribution.Id" \
  --output text)
  
# Something like F11H1ZUR2NWO24
echo "CloudFront distribution created. ID: $DIST_ID"
# Retrieve the domain name of the new distribution
DIST_DOMAIN=$(aws cloudfront get-distribution \
  --id "$DIST_ID" \
  --query "Distribution.DomainName" \
  --output text)
# Something like f2gzio890nkokl.cloudfront.net
echo "CloudFront distribution domain name: $DIST_DOMAIN"
With the static hosting now established, create the CI/CD user
echo "==> Creating CI/CD user"
DIST_ARN=$(
  aws cloudfront get-distribution \
    --id "$DIST_ID" \
    --query "Distribution.ARN" \
    --output text
)
echo "Creating IAM user $CICDUSER ..."
aws iam create-user --user-name "$CICDUSER" || echo "User may already exist, continuing ..."
read -r -d '' POLICY <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
	{
		"Sid": "CloudFrontCreateInvalidation",
		"Effect": "Allow",
		"Action": "cloudfront:CreateInvalidation",
		"Resource": [
			"$DIST_ARN"
		]
	},
    {
      "Sid": "AllowListBucket",
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": "arn:aws:s3:::$BUCKET"
    },
    {
      "Sid": "AllowPutObjects",
      "Effect": "Allow",
      "Action": ["s3:PutObject","s3:PutObjectAcl"],
      "Resource": "arn:aws:s3:::$BUCKET/*"
    }
  ]
}
EOF
echo "Attaching inline policy $POLICY_NAME to $CICDUSER ..."
aws iam put-user-policy \
  --user-name "$CICDUSER" \
  --policy-name "$POLICY_NAME" \
  --policy-document "$POLICY"
  
read ACCESS_KEY_ID SECRET_ACCESS_KEY < <(
  aws iam create-access-key \
    --user-name "$CICDUSER" \
    --query 'AccessKey.[AccessKeyId,SecretAccessKey]' \
    --output text
)
echo "Use the access key $ACCESS_KEY_ID and secret key $SECRET_ACCESS_KEY"
echo "in your development pipeline for $CICDUSER"
Using the user, run the post-build action to update the static contents of the website
echo "==> Running CI/CD post-build action"
echo "Configuring AWS CLI profile [$PROFILE] ..."
aws configure set aws_access_key_id     "$ACCESS_KEY_ID"     --profile "$PROFILE"
aws configure set aws_secret_access_key "$SECRET_ACCESS_KEY" --profile "$PROFILE"
aws configure set region                "$REGION"            --profile "$PROFILE"
echo "Syncing the build to the S3 bucket ..."
aws s3 sync build/ s3://$BUCKET_NAME --delete --profile $PROFILE
echo "Making the new contents live ..."
aws cloudfront create-invalidation --distribution-id $DIST_ID --paths "/*" --profile $PROFILE
